Skip to content
This repository was archived by the owner on Jun 23, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions docs.quible.network/docs/usage/SDK.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const identity = await wallet.createIdentity({
certificateLifespan: 86400
})

console.log('identity id: ', identity.id.toHex())
console.log('identity id: ', identity.id.toHexString())
```

## Updating identities
Expand All @@ -54,7 +54,7 @@ const privateKey = '...'
const identityId = '...'

const wallet = Wallet.fromPrivateKey(privateKey)
const identity = Identity.fromId(identityId)
const identity = Identity.fromHexString(identityId)

await identity.update({
wallet,
Expand All @@ -72,7 +72,7 @@ import {Identity} from '@quible/sdk'

const identityId = '...'

const identity = Identity.fromId(identityId)
const identity = Identity.fromHexString(identityId)

const claims = [
'utf8 string',
Expand All @@ -86,13 +86,13 @@ const certificates = await Promise.all(claims.map(async (claim) => {
})

console.log([
certificate.toBytes(), // encoded certificate data as UInt8Array
certificate.claims[0].toBytes(), // first attested claim value as UInt8Array
certificate.claims[0].toString(), // first attested claim value encoded as utf8 string
certificate.identity.id.toHex(), // identity id in hexadecimal format
certificate.expiresAt, // expiration date in seconds since unix epoch
certificate.toJSON(), // JSON-serializable representation of an certificate
certificate.signature.toHex(), // Secp256k1 32-byte ECDSA signature from the Quible network, in hexadecimal format
certificate.toBytes(), // encoded certificate data as UInt8Array
certificate.claims[0].toBytes(), // first attested claim value as UInt8Array
certificate.claims[0].toString(), // first attested claim value encoded as utf8 string
certificate.identity.id.toHexString(), // identity id in hexadecimal format
certificate.expiresAt, // expiration date in seconds since unix epoch
certificate.toJSON(), // JSON-serializable representation of an certificate
certificate.signature.toHexString(), // Secp256k1 32-byte ECDSA signature from the Quible network, in hexadecimal format
])

return certificate
Expand Down
12 changes: 6 additions & 6 deletions sdk/apps/nft-minting-example/contracts/MyNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import "@quible/verifier-solidity-sdk/contracts/QuibleVerifier.sol";

contract MyNFT is ERC721, ERC721Enumerable, Ownable {
uint256 private _nextTokenId;
bytes32 public quirkleRoot;
bytes32 public accessListIdentityId;

constructor(address initialOwner, bytes32 _quirkleRoot)
constructor(address initialOwner, bytes32 _accessListIdentityId)
ERC721("MyNFT", "QMNFT")
Ownable(initialOwner)
{
quirkleRoot = _quirkleRoot;
accessListIdentityId = _accessListIdentityId;
}

modifier membersOnly(address to, uint64 expires_at, bytes memory signature) {
QuibleVerifier.verifyProof(quirkleRoot, to, expires_at, signature);
QuibleVerifier.verifyProof(accessListIdentityId, to, expires_at, signature);
_;
}

Expand Down Expand Up @@ -54,7 +54,7 @@ contract MyNFT is ERC721, ERC721Enumerable, Ownable {
return super.supportsInterface(interfaceId);
}

function getQuirkleRoot() public view returns (bytes32) {
return quirkleRoot;
function getAccessListIdentityId() public view returns (bytes32) {
return accessListIdentityId;
}
}
31 changes: 24 additions & 7 deletions sdk/apps/nft-minting-example/src/components/Minting.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { waitForTransactionReceipt, readContract } from '@wagmi/core'
import { useReadContract, useWriteContract, useConfig } from 'wagmi'
import MyNFTArtifacts from '../../artifacts/contracts/MyNFT.sol/MyNFT.json'
import { convertHexStringToUint8Array } from '@quible/js-sdk/lib/utils'

const Minting = (props: { accountAddress: string; tokenAddress: string }) => {
const [accessListUpdateIsPending, setAccessListUpdateIsPending] =
useState(false)
const [accessList, setAccessList] = useState<string[]>([])
const config = useConfig()
const { data: hash, writeContractAsync } = useWriteContract()

const { data: ownerAddressData, isSuccess: ownerAddressDataIsSuccess } =
useReadContract({
abi: MyNFTArtifacts.abi,
address: props.tokenAddress as unknown as `0x${string}`,
functionName: 'owner',
args: [],
})

const { data, isSuccess, refetch } = useReadContract({
abi: MyNFTArtifacts.abi,
address: props.tokenAddress as unknown as `0x${string}`,
functionName: 'balanceOf',
args: [props.accountAddress],
})

const handleAccessListUpdate = useCallback(async () => {
setAccessListUpdateIsPending(true)
setAccessListUpdateIsPending(false)
}, [])

const handleMint = useCallback(async () => {
console.log('querying quirkle root', props.tokenAddress)
const quirkleRoot = await readContract(config, {
console.log(`querying object id tokenAddress=${props.tokenAddress}`)
const identityId = await readContract(config, {
abi: MyNFTArtifacts.abi,
address: props.tokenAddress as `0x${string}`,
functionName: 'getQuirkleRoot',
functionName: 'getAccessListIdentityId',
})

console.log('got quirkle root', quirkleRoot)
console.log('got identity id', identityId)

const response = await fetch('http://localhost:9013', {
method: 'POST',
Expand All @@ -35,7 +51,7 @@ const Minting = (props: { accountAddress: string; tokenAddress: string }) => {
method: 'quible_requestCertificate',
id: 67,
params: [
[...convertHexStringToUint8Array(quirkleRoot as string)],
[...convertHexStringToUint8Array(identityId as string)],
[...convertHexStringToUint8Array(props.accountAddress.toLowerCase())],
],
}),
Expand Down Expand Up @@ -71,12 +87,13 @@ const Minting = (props: { accountAddress: string; tokenAddress: string }) => {
writeContractAsync,
])

if (!isSuccess) {
if (!isSuccess || !ownerAddressDataIsSuccess) {
return <div>Loading...</div>
}

return (
<div>
{ownerAddressData === props.accountAddress && <h1>You are the owner</h1>}
<button onClick={handleMint}>Mint</button>
<p>total NFT count: {`${data}`}</p>
{hash && <p>Transaction hash: {hash}</p>}
Expand Down
194 changes: 179 additions & 15 deletions sdk/packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { privateKeyToAccount } from 'viem/accounts'
import { EIP191Signer } from '@lukso/eip191-signer.js'
import { RawSignedTransaction, Signer } from './signing'
import {
convertHexStringToFixedLengthUint8Array,
convertHexStringToUint8Array,
convertUint8ArrayToBigInt,
convertUint8ArrayToHexString,
Expand All @@ -14,22 +15,22 @@ import {
TransactionOpCode,
TransactionOutpoint,
} from './types'
import { encodeTransaction } from './encoding'

const eip191Signer = new EIP191Signer()

export class Identity {
public id: { toBytes: () => Uint8Array; toHexString: () => string }
export type QuibleClaim = { hex: string } | { raw: Uint8Array } | string

constructor(id: Uint8Array) {
this.id = {
toBytes() {
return id
},
toHexString() {
return convertUint8ArrayToHexString(id)
},
}
}
export type QuibleIdentityUpdateParams = {
wallet: QuibleWallet
insert?: QuibleClaim[]
delete?: QuibleClaim[]
certificateLifespan?: bigint
}

export type IdentityId = {
toBytes: () => Uint8Array & { length: 32 }
toHexString: () => string
}

export type CreateIdentityParams = {
Expand Down Expand Up @@ -113,6 +114,169 @@ export class QuibleProvider {

return { signer, signingKey, outpoint }
}

async fetchOutputsByObjectId(objectId: Uint8Array & { length: 32 }): Promise<{
outpoint: TransactionOutpoint
}> {
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'quible_getUnspentObjectOutputsByObjectId',
id: 67,
params: [objectId],
}),
})

const { result } = await response.json()

if (result.outpoints.length === 0) {
throw new Error('failed to fetch outputs by object id: no outputs')
}

const outpoint: TransactionOutpoint = {
txid: new Uint8Array(result.outpoints[0].txid) as Uint8Array & {
length: 32
},
index: convertUint8ArrayToBigInt(result.outpoints[0].index),
}

return { outpoint }
}

async fetchClaimsByObjectId(objectId: Uint8Array & { length: 32 }): Promise<{
claims: Uint8Array[]
}> {
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'quible_getClaimsByObjectId',
id: 67,
params: [objectId],
}),
})

const {
result: { claims },
} = await response.json()

return { claims }
}
}

export type GetCertificateParams = {
claims: QuibleClaim[]
}

export class Identity {
public id: IdentityId

async update(params: QuibleIdentityUpdateParams) {
const { wallet } = params
const { signer: faucetSigner, outpoint: faucetOutpoint } =
await wallet.provider.fetchFaucetOutput()

const { outpoint } = await wallet.provider.fetchOutputsByObjectId(
this.id.toBytes(),
)

const transaction: TransactionContents = {
inputs: [
{ outpoint: faucetOutpoint, signatureScript: [] },
{ outpoint, signatureScript: [] },
],
outputs: [
{
type: 'Object',
data: {
objectId: {
raw: this.id.toBytes(),
mode: { type: 'Existing', permitIndex: 0n },
},
dataScript: [
// { code: 'SETCERTTTL', data: BigInt(params.certificateLifespan) },
...((params.insert ?? []).map((claim) => {
let data: Uint8Array

if (typeof claim === 'string') {
data = new TextEncoder().encode(claim)
} else if ('hex' in claim) {
data = convertHexStringToUint8Array(claim.hex)
} else {
data = claim.raw
}

return {
code: 'INSERT',
data,
}
}) as TransactionOpCode[]),
],
pubkeyScript: [
{ code: 'DUP' },
{
code: 'PUSH',
data: wallet.signer.address.toBytes(),
},
{ code: 'EQUALVERIFY' },
{ code: 'CHECKSIGVERIFY' },
],
},
},
],
locktime: 0n,
}

const faucetSignedTransaction =
await faucetSigner.signTransaction(transaction)
const walletSignedTransaction =
await wallet.signer.signTransaction(transaction)

faucetSignedTransaction.contents.inputs[1].signatureScript =
walletSignedTransaction.contents.inputs[1].signatureScript

await wallet.provider.sendTransaction(
encodeTransaction(faucetSignedTransaction.contents),
)
}

public static fromHexString(identityId: string) {
return new Identity(convertHexStringToFixedLengthUint8Array(identityId, 32))
}

public static fromUint8Array(identityId: Uint8Array) {
if (identityId.length === 32) {
return new Identity(identityId as Uint8Array & { length: 32 })
}

throw new Error('Identity.fromUint8Array: expected length 32')
}

private constructor(id: Uint8Array & { length: 32 }) {
this.id = {
toBytes() {
return id
},
toHexString() {
return convertUint8ArrayToHexString(id)
},
}
}

async getCertificate(params: GetCertificateParams) {
if (params.claims.length !== 1) {
throw new Error(
'Identity#getCertificate: only one claim per certificate allowed',
)
}
}
}

export class QuibleWallet {
Expand Down Expand Up @@ -167,11 +331,11 @@ export class QuibleWallet {
locktime: 0n,
}

const encodedSignedIdentityTransaction =
const signedIdentityTransaction =
await faucetSigner.signTransaction(identityTransaction)

await this.provider.sendTransaction(encodedSignedIdentityTransaction)
await this.provider.sendTransaction(signedIdentityTransaction.encode())

return new Identity(objectId)
return Identity.fromUint8Array(objectId)
}
}
4 changes: 3 additions & 1 deletion sdk/packages/js-sdk/src/signing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ describe('signer', () => {
)

const result = await signer.signTransaction(sampleTransaction)
expect(convertUint8ArrayToHexString(result.toBytes())).toBe(expected)
expect(convertUint8ArrayToHexString(result.encode().toBytes())).toBe(
expected,
)
})
})
Loading
Loading