diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 46f5ab5..50121d4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -21,13 +21,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: true - name: Install node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: "^16.10.0" + node-version: "22.x" - name: Install dependencies run: yarn install - name: Run test diff --git a/src/Concordium.ts b/src/Concordium.ts index e5689ba..e1401cb 100644 --- a/src/Concordium.ts +++ b/src/Concordium.ts @@ -11,7 +11,10 @@ import { serializeRegisterData, serializeTransferToPublic, serializeDeployModule, - serializeInitContract + serializeInitContract, + serializeUpdateContract, + serializeTransactionPayloads, + serializeUpdateCredentials } from "./serialization"; import BigNumber from "bignumber.js"; import { encodeInt32 } from "./utils"; @@ -51,6 +54,25 @@ const P2_LAST = 0x00; const P1_INITIAL_PACKET = 0x00; const P1_SCHEDULED_TRANSFER_PAIRS = 0x01; +// Update Credentials +const P2_CREDENTIAL_INITIAL = 0x00; +const P2_CREDENTIAL_CREDENTIAL_INDEX = 0x01; +const P2_CREDENTIAL_CREDENTIAL = 0x02; +const P2_CREDENTIAL_ID_COUNT = 0x03; +const P2_CREDENTIAL_ID = 0x04; +const P2_THRESHOLD = 0x05; + +//Deploy Credential +const P1_VERIFICATION_KEY_LENGTH = 0x0A; +const P1_VERIFICATION_KEY = 0x01; +const P1_SIGNATURE_THRESHOLD = 0x02; +const P1_AR_IDENTITY = 0x03; +const P1_CREDENTIAL_DATES = 0x04; +const P1_ATTRIBUTE_TAG = 0x05; +const P1_ATTRIBUTE_VALUE = 0x06; +const P1_LENGTH_OF_PROOFS = 0x07; +const P1_PROOFS = 0x08; +const P1_NEW_OR_EXISTING = 0x09 const INS = { // GET_VERSION: 0x03, @@ -62,11 +84,13 @@ const INS = { SIGN_CONFIGURE_DELEGATION: 0x17, SIGN_CONFIGURE_BAKER: 0x18, GET_APP_NAME: 0x21, + SIGN_UPDATE_CREDENTIALS: 0x31, SIGN_TRANSFER_MEMO: 0x32, SIGN_TRANSFER_SCHEDULE_AND_MEMO: 0x34, SIGN_REGISTER_DATA: 0x35, SIGN_DEPLOY_MODULE: 0x06, SIGN_INIT_CONTRACT: 0x06, + SIGN_UPDATE_CONTRACT: 0x06, }; /** @@ -474,7 +498,7 @@ export default class Concordium { }; } - async signDeployModule(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signDeployModule(txn, path: string): Promise<{ signature: string[] }> { const { payloads } = serializeDeployModule(txn, path); @@ -492,15 +516,12 @@ export default class Concordium { if (response.length === 1) throw new Error("User has declined."); - const transaction = payloads.slice(1); - return { signature: response.toString("hex"), - transaction: Buffer.concat(transaction).toString("hex"), }; } - async signInitContract(txn, path: string): Promise<{ signature: string[]; transaction }> { + async signInitContract(txn, path: string): Promise<{ signature: string[] }> { const { payloads } = serializeInitContract(txn, path); @@ -518,11 +539,141 @@ export default class Concordium { if (response.length === 1) throw new Error("User has declined."); - const transaction = payloads.slice(1); + return { + signature: response.toString("hex"), + }; + } + + async signUpdateContract(txn, path: string): Promise<{ signature: string[] }> { + + const { payloads } = serializeUpdateContract(txn, path); + + let response; + + for (let i = 0; i < payloads.length; i++) { + const lastChunk = i === payloads.length - 1; + response = await this.sendToDevice( + INS.SIGN_UPDATE_CONTRACT, + P1_FIRST_CHUNK + i, + lastChunk ? P2_LAST : P2_MORE, + payloads[i] + ); + } + + if (response.length === 1) throw new Error("User has declined."); + + return { + signature: response.toString("hex"), + }; + } + + async signUpdateCredentials(txn, path: string): Promise<{ signature: string[] }> { + + const { payloadHeaderKindAndIndexLength, credentialIndex, numberOfVerificationKeys, keyIndexAndSchemeAndVerificationKey, thresholdAndRegIdAndIPIdentity, encIdCredPubShareAndKey, validToAndCreatedAtAndAttributesLength, tag, valueLength, value, proofLength, proofs, credentialIdCount, credentialIds, threshold } = serializeUpdateCredentials(txn, path); + + let response; + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_INITIAL, + payloadHeaderKindAndIndexLength[0] + ); + + for (let i = 0; i < txn.payload.newCredentials.length; i++) { + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_CREDENTIAL_INDEX, + credentialIndex[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_VERIFICATION_KEY_LENGTH, + P2_CREDENTIAL_CREDENTIAL, + numberOfVerificationKeys[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_VERIFICATION_KEY, + P2_CREDENTIAL_CREDENTIAL, + keyIndexAndSchemeAndVerificationKey[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_SIGNATURE_THRESHOLD, + P2_CREDENTIAL_CREDENTIAL, + thresholdAndRegIdAndIPIdentity[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_AR_IDENTITY, + P2_CREDENTIAL_CREDENTIAL, + encIdCredPubShareAndKey[i] + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_CREDENTIAL_DATES, + P2_CREDENTIAL_CREDENTIAL, + validToAndCreatedAtAndAttributesLength[i] + ); + for (let j = 0; j < Object.keys(txn.payload.newCredentials[i].cdi.policy.revealedAttributes).length; j++) { + const tagAndValueLength = Buffer.concat([tag[i][j], valueLength[i][j]]) + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_ATTRIBUTE_TAG, + P2_CREDENTIAL_CREDENTIAL, + tagAndValueLength + ); + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_ATTRIBUTE_VALUE, + P2_CREDENTIAL_CREDENTIAL, + value[i][j] + ); + } + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_LENGTH_OF_PROOFS, + P2_CREDENTIAL_CREDENTIAL, + proofLength[i] + ); + + const proofPayload = serializeTransactionPayloads(proofs[i]); + for (let j = 0; j < proofPayload.length; j++) { + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + P1_PROOFS, + P2_CREDENTIAL_CREDENTIAL, + proofPayload[j] + ); + } + } + + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_ID_COUNT, + credentialIdCount + ); + for (let i = 0; i < txn.payload.removeCredentialIds.length; i++) { + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_CREDENTIAL_ID, + credentialIds[i] + ); + } + response = await this.sendToDevice( + INS.SIGN_UPDATE_CREDENTIALS, + NONE, + P2_THRESHOLD, + threshold + ); + + if (response.length === 1) throw new Error("User has declined."); return { signature: response.toString("hex"), - transaction: Buffer.concat(transaction).toString("hex"), }; } diff --git a/src/serialization.ts b/src/serialization.ts index 237e3ae..29386f6 100644 --- a/src/serialization.ts +++ b/src/serialization.ts @@ -8,6 +8,8 @@ const MAX_CHUNK_SIZE = 255; const MAX_SCHEDULE_CHUNK_SIZE = 15; const HEADER_LENGTH = 60; const TRANSACTION_KIND_LENGTH = 1; +const INDEX_LENGTH = 1; +const ONE_OCTET_LENGTH = 1; const BITMAP_LENGTH = 2; const STAKING_PAYLOAD_LENGTH = 8; const RESTAKE_EARNINGS_PAYLOAD_LENGTH = 1; @@ -18,7 +20,20 @@ const KEYS_PAYLOAD_LENGTH = KEYS_ELECTION_AND_SIGNATURE_LENGTH + KEYS_AGGREGATIO const METADATA_URL_LENGTH = 2; const TRANSACTION_FEE_COMMISSION_LENGTH = 4; const BAKING_REWARD_COMMISSION_LENGTH = 4; +const REVOCATION_THRESHOLD_LENGTH = 4; const FINALIZATION_REWARD_COMMISSION_LENGTH = 4; +const KEY_LENGTH = 32; +const REG_ID_LENGTH = 48; +const IP_IDENTITY_LENGTH = 4; +const AR_DATA_LENGTH = 2; +const ID_CRED_PUB_SHARE_LENGTH = 96; +const VALID_TO_LENGTH = 3; +const CREATED_AT_LENGTH = 3; +const ATTRIBUTES_LENGTH = 2; +const TAG_LENGTH = 1; +const VALUE_LENGTH = 1; +const PROOF_LENGTH_LENGTH = 4; +const CREDENTIAL_ID_LENGTH = 48; const serializePath = (path: number[]): Buffer => { const buf = Buffer.alloc(1 + path.length * 4); @@ -86,7 +101,7 @@ const serializeTransactionPayloadsWithDerivationPath = (path: string, rawTx: Buf }; -const serializeTransactionPayloads = (rawTx: Buffer): Buffer[] => { +export const serializeTransactionPayloads = (rawTx: Buffer): Buffer[] => { let offset = 0; const payloads: Buffer[] = []; while (offset !== rawTx.length) { @@ -323,4 +338,72 @@ export const serializeDeployModule = (txn: any, path: string): { payloads: Buffe export const serializeInitContract = (txn: any, path: string): { payloads: Buffer[] } => { return serializeTransaction(txn, path); -}; \ No newline at end of file +}; + +export const serializeUpdateContract = (txn: any, path: string): { payloads: Buffer[] } => { + return serializeTransaction(txn, path); +}; + +export const serializeUpdateCredentials = (txn: any, path: string): { payloadHeaderKindAndIndexLength: Buffer[], credentialIndex: Buffer[], numberOfVerificationKeys: Buffer[], keyIndexAndSchemeAndVerificationKey: Buffer[], thresholdAndRegIdAndIPIdentity: Buffer[], encIdCredPubShareAndKey: Buffer[], validToAndCreatedAtAndAttributesLength: Buffer[], attributesLength: Buffer[], tag: Buffer[][], valueLength: Buffer[][], value: Buffer[][], proofLength: Buffer[], proofs: Buffer[], credentialIdCount: Buffer, credentialIds: Buffer[], threshold: Buffer } => { + let offset = 0; + const txSerialized = serializeAccountTransaction(txn); + const headerKindAndIndexLength = txSerialized.subarray(offset, offset + HEADER_LENGTH + TRANSACTION_KIND_LENGTH + INDEX_LENGTH); + const payloadHeaderKindAndIndexLength = serializeTransactionPayloadsWithDerivationPath(path, headerKindAndIndexLength); + offset += HEADER_LENGTH + TRANSACTION_KIND_LENGTH + INDEX_LENGTH; + + let credentialIndex: Buffer[] = []; + let numberOfVerificationKeys: Buffer[] = []; + let keyIndexAndSchemeAndVerificationKey: Buffer[] = []; + let thresholdAndRegIdAndIPIdentity: Buffer[] = []; + let encIdCredPubShareAndKey: Buffer[] = []; + let validToAndCreatedAtAndAttributesLength: Buffer[] = []; + let attributesLength: Buffer[] = []; + let tag: Buffer[][] = [[]]; + let valueLength: Buffer[][] = [[]]; + let value: Buffer[][] = [[]]; + let proofLength: Buffer[] = []; + let proofs: Buffer[] = []; + + for (let i = 0; i < txn.payload.newCredentials.length; i++) { + credentialIndex[i] = txSerialized.subarray(offset, offset + INDEX_LENGTH); + offset += INDEX_LENGTH; + numberOfVerificationKeys[i] = txSerialized.subarray(offset, offset + INDEX_LENGTH); + offset += INDEX_LENGTH; + keyIndexAndSchemeAndVerificationKey[i] = txSerialized.subarray(offset, offset + 2 * ONE_OCTET_LENGTH + KEY_LENGTH); + offset += 2 * ONE_OCTET_LENGTH + KEY_LENGTH; + thresholdAndRegIdAndIPIdentity[i] = txSerialized.subarray(offset, offset + 2 * ONE_OCTET_LENGTH + REG_ID_LENGTH + IP_IDENTITY_LENGTH + AR_DATA_LENGTH); + offset += 2 * ONE_OCTET_LENGTH + REG_ID_LENGTH + IP_IDENTITY_LENGTH + AR_DATA_LENGTH; + encIdCredPubShareAndKey[i] = txSerialized.subarray(offset, offset + 4 * ONE_OCTET_LENGTH + ID_CRED_PUB_SHARE_LENGTH); + offset += 4 * ONE_OCTET_LENGTH + ID_CRED_PUB_SHARE_LENGTH; + validToAndCreatedAtAndAttributesLength[i] = txSerialized.subarray(offset, offset + ATTRIBUTES_LENGTH + VALID_TO_LENGTH + CREATED_AT_LENGTH); + offset += ATTRIBUTES_LENGTH + VALID_TO_LENGTH + CREATED_AT_LENGTH; + attributesLength[i] = validToAndCreatedAtAndAttributesLength[i].subarray(-ATTRIBUTES_LENGTH); + tag[i] = []; + valueLength[i] = []; + value[i] = []; + for (let j = 0; j < attributesLength[i].readUInt16BE(0); j++) { + tag[i].push(txSerialized.subarray(offset, offset + TAG_LENGTH)); + offset += TAG_LENGTH; + valueLength[i].push(txSerialized.subarray(offset, offset + VALUE_LENGTH)); + offset += VALUE_LENGTH; + value[i].push(txSerialized.subarray(offset, offset + valueLength[i][j].readUInt8(0))); + offset += valueLength[i][j].readUInt8(0); + } + + proofLength[i] = txSerialized.subarray(offset, offset + PROOF_LENGTH_LENGTH); + offset += PROOF_LENGTH_LENGTH; + proofs[i] = txSerialized.subarray(offset, offset + proofLength[i].readUInt32BE(0)); + offset += proofLength[i].readUInt32BE(0); + } + const credentialIdCount = txSerialized.subarray(offset, offset + ONE_OCTET_LENGTH); + offset += ONE_OCTET_LENGTH; + + const credentialIds: Buffer[] = []; + for (let i = 0; i < credentialIdCount.readUInt8(0); i++) { + credentialIds.push(txSerialized.subarray(offset, offset + CREDENTIAL_ID_LENGTH)); + offset += CREDENTIAL_ID_LENGTH; + } + const threshold = txSerialized.subarray(offset, offset + ONE_OCTET_LENGTH); + offset += ONE_OCTET_LENGTH; + return { payloadHeaderKindAndIndexLength, credentialIndex, numberOfVerificationKeys, keyIndexAndSchemeAndVerificationKey, thresholdAndRegIdAndIPIdentity, encIdCredPubShareAndKey, validToAndCreatedAtAndAttributesLength, attributesLength, tag, valueLength, value, proofLength, proofs, credentialIdCount, credentialIds, threshold }; +};