From 2df64002b6634ea6e4cdb4c3afbf4a9ab697de34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 28 Apr 2023 10:39:47 +0200 Subject: [PATCH 1/8] Create build function for AccountSigner from different formats --- packages/common/src/signHelpers.ts | 98 ++++++++++++++++++++++++++++-- packages/common/src/types.ts | 30 +++++++++ 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/packages/common/src/signHelpers.ts b/packages/common/src/signHelpers.ts index 5c5324358..42cc421d7 100644 --- a/packages/common/src/signHelpers.ts +++ b/packages/common/src/signHelpers.ts @@ -1,23 +1,51 @@ import { getAccountTransactionSignDigest } from './serialization'; import { AccountInfo, + AccountKeys, AccountTransaction, AccountTransactionSignature, + CredentialKeys, + CredentialSignature, + HexString, + WalletExportFormat, + WithAccountKeys, } from './types'; import * as ed from '@noble/ed25519'; import { Buffer } from 'buffer/'; import { AccountAddress } from './types/accountAddress'; import { sha256 } from './hash'; +/** + * A structure to use for creating signatures on a given digest. + */ export interface AccountSigner { + /** + * Creates a signature of the provided digest + * + * @param {Buffer} digest - The digest to create signatures on. + * + * @returns {Promise} A promise resolving with a set of signatures for a set of credentials corresponding to some account + */ sign(digest: Buffer): Promise; + /** + * Amount of signatures created + */ getSignatureCount(): bigint; } +const getSignature = async ( + digest: Buffer, + privateKey: string +): Promise => + Buffer.from(await ed.sign(digest, privateKey)).toString('hex'); + /** - * Creates a signer for an account which uses the first credential's first keypair. + * Creates an `AccountSigner` for an account which uses the first credential's first keypair. * Note that if the account has a threshold > 1 or the first credentials has a threshold > 1, the transaction signed using this will fail. + * * @param privateKey the ed25519 private key in HEX format. (First credential's first keypair's private key) + * + * @returns {AccountSigner} an `AccountSigner` which creates a signature using the first credentials first keypair */ export function buildBasicAccountSigner(privateKey: string): AccountSigner { return { @@ -25,18 +53,78 @@ export function buildBasicAccountSigner(privateKey: string): AccountSigner { return 1n; }, async sign(digest: Buffer) { - const signature = Buffer.from( - await ed.sign(digest, privateKey) - ).toString('hex'); return { 0: { - 0: signature, + 0: await getSignature(digest, privateKey), }, }; }, }; } +const isWalletExport = ( + value: T | WalletExportFormat +): value is WalletExportFormat => + (value as WalletExportFormat).value?.accountKeys !== undefined; + +const getCredentialSignature = async ( + digest: Buffer, + { keys }: CredentialKeys +): Promise => { + const sig: CredentialSignature = {}; + for (const key in keys) { + sig[key] = await getSignature(digest, keys[key].signKey); + } + return sig; +}; + +/** + * Creates an `AccountSigner` for an account exported from a Concordium wallet. + * Creating signatures using the `AccountSigner` will hold signatures for all credentials and all their respective keys included in the export. + * + * @param {WalletExportFormat} walletExport - The wallet export object. + * + * @returns {AccountSigner} an `AccountSigner` which creates signatures using all keys for all credentials + */ +export function buildAccountSigner( + walletExport: WalletExportFormat +): AccountSigner; +/** + * Creates a signer for an arbitrary format extending the {@link WithAccountKeys} type. + * Creating signatures using the `AccountSigner` will hold signatures for all credentials and all their respective keys included. + * + * @param {AccountKeys} value.accountKeys - Account keys of type {@link AccountKeys} to use for creating signatures + * + * @returns {AccountSigner} an `AccountSigner` which creates signatures using all keys for all credentials + */ +export function buildAccountSigner( + value: T +): AccountSigner; +export function buildAccountSigner( + value: T | WalletExportFormat +): AccountSigner { + const { keys }: AccountKeys = isWalletExport(value) + ? value.value.accountKeys + : value.accountKeys; + + const numKeys = Object.values(keys).reduce( + (acc, credKeys) => acc + BigInt(Object.keys(credKeys).length), + 0n + ); + return { + getSignatureCount() { + return numKeys; + }, + async sign(digest: Buffer) { + const sig: AccountTransactionSignature = {}; + for (const key in keys) { + sig[key] = await getCredentialSignature(digest, keys[key]); + } + return sig; + }, + }; +} + /** * Helper function to sign an AccountTransaction. * @param transaction the account transaction to sign diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 36f7c3588..c6a6de797 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -706,11 +706,41 @@ export interface VerifyKey { verifyKey: string; } +export interface KeyPair { + signKey: string; + verifyKey: string; +} + export interface CredentialPublicKeys { keys: Record; threshold: number; } +export interface CredentialKeys { + keys: Record; + threshold: number; +} + +export interface AccountKeys { + keys: Record; + threshold: number; +} + +export interface WithAccountKeys { + accountKeys: AccountKeys; +} + +export interface WalletExportFormat { + type: string; + v: number; + environment: string; + value: { + accountKeys: AccountKeys; + address: Base58String; + credentials: Record; + }; +} + export interface ChainArData { encIdCredPubShare: string; } From 86cf14d3a903dff5a175c9c51670dfbf5f81363f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 28 Apr 2023 11:18:31 +0200 Subject: [PATCH 2/8] Add unit tests for signing with keys from object conforming to export formats --- packages/common/test/signHelpers.test.ts | 155 ++++++++++++++++++++--- 1 file changed, 134 insertions(+), 21 deletions(-) diff --git a/packages/common/test/signHelpers.test.ts b/packages/common/test/signHelpers.test.ts index 465ad90dc..2eab5eac9 100644 --- a/packages/common/test/signHelpers.test.ts +++ b/packages/common/test/signHelpers.test.ts @@ -3,9 +3,51 @@ import { buildBasicAccountSigner, verifyMessageSignature, } from '../src/signHelpers'; -import { AccountAddress, AccountInfo } from '../src'; +import { + AccountAddress, + AccountInfo, + buildAccountSigner, + WithAccountKeys, +} from '../src'; + +const TEST_ACCOUNT_SINGLE = + '3eP94feEdmhYiPC1333F9VoV31KGMswonuHk5tqmZrzf761zK5'; +const TEST_ACCOUNT_MULTI = '4hTGW1Uz6u2hUgEtwWjJUdZQncVpHGWZPgGdRpgL1VNn5NzyHd'; -const TEST_CREDENTIALS = { +const TEST_KEY_SINGLE = + 'e1cf504954663e49f4fe884c7c35415b09632cccd82d3d2a62ab2825e67d785d'; +const TEST_KEYS_MULTI: WithAccountKeys = { + accountKeys: { + keys: { + 0: { + keys: { + 0: { + signKey: + '671eb13486ea747a1c27984aca67778508dcf54bdac00a32fd138ef69ad2e5b5', + verifyKey: + '008739a5c6708b25c359d45179fefda7ef1345099c0ad8e9b66ed253d968098d', + }, + 1: { + signKey: + '76cc8d4202810aa60109435d83357751f3108d00d27d0d6cae07ab536cf6731d', + verifyKey: + '45b55ad7438cb72c06489be231443cb3b7708f9b3f770729e2092f78ea9e2d9d', + }, + 2: { + signKey: + '131a05cab3b2b18a867ae3e245881cb0f2cf2924ae33a9fa948d1451c2bd8707', + verifyKey: + 'ed2f710f6edbf65806eaee6d643a12124332a6dc687be099b63fd0150294168d', + }, + }, + threshold: 3, + }, + }, + threshold: 1, + }, +}; + +const TEST_CREDENTIALS_SINGLE = { 0: { value: { contents: { @@ -23,29 +65,64 @@ const TEST_CREDENTIALS = { }, }, }; +const TEST_CREDENTIALS_MULTI = { + 0: { + value: { + contents: { + credentialPublicKeys: { + keys: { + 0: { + schemeId: 'Ed25519', + verifyKey: + '008739a5c6708b25c359d45179fefda7ef1345099c0ad8e9b66ed253d968098d', + }, + 1: { + schemeId: 'Ed25519', + verifyKey: + '45b55ad7438cb72c06489be231443cb3b7708f9b3f770729e2092f78ea9e2d9d', + }, + 2: { + schemeId: 'Ed25519', + verifyKey: + 'ed2f710f6edbf65806eaee6d643a12124332a6dc687be099b63fd0150294168d', + }, + }, + threshold: 3, + }, + }, + }, + }, +}; const testEachMessageType = test.each(['test', Buffer.from('test', 'utf8')]); testEachMessageType('[%o] test signMessage', async (message) => { - const account = new AccountAddress( - '3eP94feEdmhYiPC1333F9VoV31KGMswonuHk5tqmZrzf761zK5' - ); - const signature = await signMessage( - account, - message, - buildBasicAccountSigner( - 'e1cf504954663e49f4fe884c7c35415b09632cccd82d3d2a62ab2825e67d785d' - ) - ); + const sign = () => signMessage(account, message, signer); + + let account = new AccountAddress(TEST_ACCOUNT_SINGLE); + let signer = buildBasicAccountSigner(TEST_KEY_SINGLE); + let signature = await sign(); expect(signature[0][0]).toBe( '445197d79ca90d8cc8440328dac9f307932ade0c03cc7aa575b59b746e26e5f1bca13ade5ff7a56e918ba5a32450fdf52b034cd2580929b21213263e81f7f809' ); + + account = new AccountAddress(TEST_ACCOUNT_MULTI); + signer = buildAccountSigner(TEST_KEYS_MULTI); + signature = await sign(); + + expect(signature).toEqual({ + 0: { + 0: '37798d551f26f48496a3d14aee0d29f5bb6a1dc99a75c06b5a8be4f901ba8e6e7c32a7461bd419f481115e647a43d43075f0ccb000627eaa2329eed81582fc02', + 1: '36fc3a13869535a934adb61809b010dd015126920c24032dfcde1c3883151bc61219f2582564f1e13d743a34ce762925d6171685a1fec62e1cbf731e551a430f', + 2: '024c91adf278e9018f27546da73acf865823989b2385dd9575743c1390dda1afa47e85894ac2324bd9cd5459393b69a18787c18262ac90d65b404245491c6b0c', + }, + }); }); testEachMessageType( '[%o] verifyMessageSignature returns true on the correct address/signature', async (message) => { - const signature = await verifyMessageSignature( + const signatureSingle = await verifyMessageSignature( message, { 0: { @@ -53,13 +130,29 @@ testEachMessageType( }, }, { - accountAddress: - '3eP94feEdmhYiPC1333F9VoV31KGMswonuHk5tqmZrzf761zK5', + accountAddress: TEST_ACCOUNT_SINGLE, + accountThreshold: 1, + accountCredentials: TEST_CREDENTIALS_SINGLE, + } as unknown as AccountInfo + ); + expect(signatureSingle).toBeTruthy(); + + const signatureMutli = await verifyMessageSignature( + message, + { + 0: { + 0: '37798d551f26f48496a3d14aee0d29f5bb6a1dc99a75c06b5a8be4f901ba8e6e7c32a7461bd419f481115e647a43d43075f0ccb000627eaa2329eed81582fc02', + 1: '36fc3a13869535a934adb61809b010dd015126920c24032dfcde1c3883151bc61219f2582564f1e13d743a34ce762925d6171685a1fec62e1cbf731e551a430f', + 2: '024c91adf278e9018f27546da73acf865823989b2385dd9575743c1390dda1afa47e85894ac2324bd9cd5459393b69a18787c18262ac90d65b404245491c6b0c', + }, + }, + { + accountAddress: TEST_ACCOUNT_MULTI, accountThreshold: 1, - accountCredentials: TEST_CREDENTIALS, + accountCredentials: TEST_CREDENTIALS_MULTI, } as unknown as AccountInfo ); - expect(signature).toBeTruthy(); + expect(signatureMutli).toBeTruthy(); } ); @@ -76,7 +169,7 @@ test('verifyMessageSignature returns false on the incorrect address', async () = accountAddress: '3dbRxtzhb8MotFBgH5DcdFJy7t4we4N8Ep6Mxdha8XvLhq7YmZ', accountThreshold: 1, - accountCredentials: TEST_CREDENTIALS, + accountCredentials: TEST_CREDENTIALS_SINGLE, } as unknown as AccountInfo ); expect(signature).toBeFalsy(); @@ -92,11 +185,31 @@ test('verifyMessageSignature returns false on the incorrect signature', async () }, }, { - accountAddress: - '3eP94feEdmhYiPC1333F9VoV31KGMswonuHk5tqmZrzf761zK5', + accountAddress: TEST_ACCOUNT_SINGLE, accountThreshold: 1, - accountCredentials: TEST_CREDENTIALS, + accountCredentials: TEST_CREDENTIALS_SINGLE, } as unknown as AccountInfo ); expect(signature).toBeFalsy(); }); + +testEachMessageType( + '[%o] verifyMessageSignature returns false on not enough signatures', + async (message) => { + const signature = await verifyMessageSignature( + message, + { + 0: { + 0: '37798d551f26f48496a3d14aee0d29f5bb6a1dc99a75c06b5a8be4f901ba8e6e7c32a7461bd419f481115e647a43d43075f0ccb000627eaa2329eed81582fc02', + 1: '36fc3a13869535a934adb61809b010dd015126920c24032dfcde1c3883151bc61219f2582564f1e13d743a34ce762925d6171685a1fec62e1cbf731e551a430f', + }, + }, + { + accountAddress: TEST_ACCOUNT_MULTI, + accountThreshold: 1, + accountCredentials: TEST_CREDENTIALS_MULTI, + } as unknown as AccountInfo + ); + expect(signature).toBeFalsy(); + } +); From 3d705d43971924c349ee7be91e5da921d555ecab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 28 Apr 2023 13:28:38 +0200 Subject: [PATCH 3/8] Document building AccountSigner from more sources --- packages/common/README.md | 25 ++++++++- packages/common/src/signHelpers.ts | 71 ++++++++++++++++++++---- packages/common/src/types.ts | 8 ++- packages/common/test/signHelpers.test.ts | 35 ++---------- 4 files changed, 92 insertions(+), 47 deletions(-) diff --git a/packages/common/README.md b/packages/common/README.md index 98e49b8ec..04c6a5030 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -607,6 +607,28 @@ if (deserialized.kind === BlockItemKind.AccountTransactionKind) { Note that currently the only supported account transaction kinds are `Transfer`, `TransferWithMemo` and `RegisterData`. If attempting to deserialize other transaction kinds, the function will throw an error; +## Creating an `AccountSigner` +It is possible to build an `AccountSigner` in a variety of ways by utilizing the function `buildAccountSigner`. For a simple account, with a single credential and one keypair in the credential, one can supply a single private key, like so: + +```js +const privateKey = '...'; // Private key of an account as hex string +const signer: AccountSigner = buildAccountSigner(privateKey); +``` + +For a more complex account with one or more credentials, each with one or more keypairs, `buildAccountSigner` is also compatible with the format created by the chain genesis tool, Concordium wallet exports, along with a map of type `SimpleAccountKeys`. + +```js +const keys: SimpleAccountKeys = { + 0: { + 0: '...', // Private key of an account as hex string + 1: '...', + ... + }, + ... +}; +const signer: AccountSigner = buildAccountSigner(keys); +``` + ## Sign an account transaction The following example demonstrates how to use the `signTransaction` helper function to sign a account transaction: @@ -621,9 +643,6 @@ const transactionSignature: AccountTransactionSignature = signTransaction(accoun sendTransaction(accountTransaction, transactionSignature); ``` -For a simple account, with a single credential and one keypair in the credential, one can use the `buildBasicAccountSigner` to get the signer. Otherwise one needs to implement the AccountSigner interface themselves, for now. -The `buildBasicAccountSigner` function take the account's private key as a hex string. - The following is an example of how to sign an account transaction without using the `signTransaction` helper function: ```js import * as ed from "@noble/ed25519"; diff --git a/packages/common/src/signHelpers.ts b/packages/common/src/signHelpers.ts index 42cc421d7..71e4438cb 100644 --- a/packages/common/src/signHelpers.ts +++ b/packages/common/src/signHelpers.ts @@ -1,12 +1,11 @@ import { getAccountTransactionSignDigest } from './serialization'; import { AccountInfo, - AccountKeys, AccountTransaction, AccountTransactionSignature, - CredentialKeys, CredentialSignature, HexString, + SimpleAccountKeys, WalletExportFormat, WithAccountKeys, } from './types'; @@ -43,7 +42,7 @@ const getSignature = async ( * Creates an `AccountSigner` for an account which uses the first credential's first keypair. * Note that if the account has a threshold > 1 or the first credentials has a threshold > 1, the transaction signed using this will fail. * - * @param privateKey the ed25519 private key in HEX format. (First credential's first keypair's private key) + * @param {HexString} privateKey - the ed25519 private key in HEX format. (First credential's first keypair's private key) * * @returns {AccountSigner} an `AccountSigner` which creates a signature using the first credentials first keypair */ @@ -67,13 +66,41 @@ const isWalletExport = ( ): value is WalletExportFormat => (value as WalletExportFormat).value?.accountKeys !== undefined; +const isSimpleAccountKeys = ( + value: T | WalletExportFormat | SimpleAccountKeys +): value is SimpleAccountKeys => + (value as WalletExportFormat).value?.accountKeys === undefined && + (value as T).accountKeys === undefined; + +const getKeys = ( + value: T | WalletExportFormat | SimpleAccountKeys +): SimpleAccountKeys => { + if (isSimpleAccountKeys(value)) { + return value; + } + const { keys } = isWalletExport(value) + ? value.value.accountKeys + : value.accountKeys; + + return Object.entries(keys).reduce( + (acc, [k, v]) => ({ + ...acc, + [k]: Object.entries(v.keys).reduce( + (a, e) => ({ ...a, [e[0]]: e[1].signKey }), + {} + ), + }), + {} + ); +}; + const getCredentialSignature = async ( digest: Buffer, - { keys }: CredentialKeys + keys: Record ): Promise => { const sig: CredentialSignature = {}; for (const key in keys) { - sig[key] = await getSignature(digest, keys[key].signKey); + sig[key] = await getSignature(digest, keys[key]); } return sig; }; @@ -84,33 +111,53 @@ const getCredentialSignature = async ( * * @param {WalletExportFormat} walletExport - The wallet export object. * - * @returns {AccountSigner} an `AccountSigner` which creates signatures using all keys for all credentials + * @returns {AccountSigner} An `AccountSigner` which creates signatures using all keys for all credentials */ export function buildAccountSigner( walletExport: WalletExportFormat ): AccountSigner; /** - * Creates a signer for an arbitrary format extending the {@link WithAccountKeys} type. + * Creates an `AccountSigner` for an arbitrary format extending the {@link WithAccountKeys} type. * Creating signatures using the `AccountSigner` will hold signatures for all credentials and all their respective keys included. * * @param {AccountKeys} value.accountKeys - Account keys of type {@link AccountKeys} to use for creating signatures * - * @returns {AccountSigner} an `AccountSigner` which creates signatures using all keys for all credentials + * @returns {AccountSigner} An `AccountSigner` which creates signatures using all keys for all credentials */ export function buildAccountSigner( value: T ): AccountSigner; +/** + * Creates an `AccountSigner` for the {@link SimpleAccountKeys} type. + * Creating signatures using the `AccountSigner` will hold signatures for all credentials and all their respective keys included. + * + * @param {SimpleAccountKeys} keys - Account keys to use for creating signatures + * + * @returns {AccountSigner} An `AccountSigner` which creates signatures using all keys for all credentials + */ +export function buildAccountSigner(keys: SimpleAccountKeys): AccountSigner; +/** + * Creates an `AccountSigner` for an account which uses the first credential's first keypair. + * Note that if the account has a threshold > 1 or the first credentials has a threshold > 1, the transaction signed using this will fail. + * + * @param {HexString} key - The ed25519 private key in HEX format. (First credential's first keypair's private key) + * + * @returns {AccountSigner} An `AccountSigner` which creates a signature using the first credentials first keypair + */ +export function buildAccountSigner(key: HexString): AccountSigner; export function buildAccountSigner( - value: T | WalletExportFormat + value: T | WalletExportFormat | SimpleAccountKeys | string ): AccountSigner { - const { keys }: AccountKeys = isWalletExport(value) - ? value.value.accountKeys - : value.accountKeys; + if (typeof value === 'string') { + return buildBasicAccountSigner(value); + } + const keys = getKeys(value); const numKeys = Object.values(keys).reduce( (acc, credKeys) => acc + BigInt(Object.keys(credKeys).length), 0n ); + return { getSignatureCount() { return numKeys; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index c6a6de797..5678db066 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -703,12 +703,12 @@ export interface AccountEncryptedAmount { export interface VerifyKey { schemeId: string; - verifyKey: string; + verifyKey: HexString; } export interface KeyPair { - signKey: string; - verifyKey: string; + signKey: HexString; + verifyKey: HexString; } export interface CredentialPublicKeys { @@ -726,6 +726,8 @@ export interface AccountKeys { threshold: number; } +export type SimpleAccountKeys = Record>; + export interface WithAccountKeys { accountKeys: AccountKeys; } diff --git a/packages/common/test/signHelpers.test.ts b/packages/common/test/signHelpers.test.ts index 2eab5eac9..ca57f383a 100644 --- a/packages/common/test/signHelpers.test.ts +++ b/packages/common/test/signHelpers.test.ts @@ -7,7 +7,7 @@ import { AccountAddress, AccountInfo, buildAccountSigner, - WithAccountKeys, + SimpleAccountKeys, } from '../src'; const TEST_ACCOUNT_SINGLE = @@ -16,34 +16,11 @@ const TEST_ACCOUNT_MULTI = '4hTGW1Uz6u2hUgEtwWjJUdZQncVpHGWZPgGdRpgL1VNn5NzyHd'; const TEST_KEY_SINGLE = 'e1cf504954663e49f4fe884c7c35415b09632cccd82d3d2a62ab2825e67d785d'; -const TEST_KEYS_MULTI: WithAccountKeys = { - accountKeys: { - keys: { - 0: { - keys: { - 0: { - signKey: - '671eb13486ea747a1c27984aca67778508dcf54bdac00a32fd138ef69ad2e5b5', - verifyKey: - '008739a5c6708b25c359d45179fefda7ef1345099c0ad8e9b66ed253d968098d', - }, - 1: { - signKey: - '76cc8d4202810aa60109435d83357751f3108d00d27d0d6cae07ab536cf6731d', - verifyKey: - '45b55ad7438cb72c06489be231443cb3b7708f9b3f770729e2092f78ea9e2d9d', - }, - 2: { - signKey: - '131a05cab3b2b18a867ae3e245881cb0f2cf2924ae33a9fa948d1451c2bd8707', - verifyKey: - 'ed2f710f6edbf65806eaee6d643a12124332a6dc687be099b63fd0150294168d', - }, - }, - threshold: 3, - }, - }, - threshold: 1, +const TEST_KEYS_MULTI: SimpleAccountKeys = { + 0: { + 0: '671eb13486ea747a1c27984aca67778508dcf54bdac00a32fd138ef69ad2e5b5', + 1: '76cc8d4202810aa60109435d83357751f3108d00d27d0d6cae07ab536cf6731d', + 2: '131a05cab3b2b18a867ae3e245881cb0f2cf2924ae33a9fa948d1451c2bd8707', }, }; From b6acdfcee6b165568b24c89a76ec82159bbb0679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 28 Apr 2023 13:43:14 +0200 Subject: [PATCH 4/8] Changelog updated --- packages/common/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index a60dcdac7..b58457dfa 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +### Added +- Build function `buildAccountSigner` for creating `AccountSigner` objects from genesis format, wallet export format, and a simple representation of credentials with keys. + ## 6.4.2 2023-04-21 ### Changed From e62153980fb38a129b16b459c9f3c4bf272ef477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 28 Apr 2023 13:46:35 +0200 Subject: [PATCH 5/8] Update nodejs deps --- packages/nodejs/package.json | 2 +- yarn.lock | 22 +--------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json index eb242cfa4..ac4b22c35 100644 --- a/packages/nodejs/package.json +++ b/packages/nodejs/package.json @@ -60,7 +60,7 @@ "build-dev": "tsc" }, "dependencies": { - "@concordium/common-sdk": "6.4.1", + "@concordium/common-sdk": "6.4.2", "@grpc/grpc-js": "^1.3.4", "@protobuf-ts/grpc-transport": "^2.8.2", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index abbef8670..75e3c00c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1370,31 +1370,11 @@ __metadata: languageName: node linkType: hard -"@concordium/common-sdk@npm:6.4.1": - version: 6.4.1 - resolution: "@concordium/common-sdk@npm:6.4.1" - dependencies: - "@concordium/rust-bindings": 0.11.0 - "@grpc/grpc-js": ^1.3.4 - "@noble/ed25519": ^1.7.1 - "@protobuf-ts/runtime-rpc": ^2.8.2 - "@scure/bip39": ^1.1.0 - bs58check: ^2.1.2 - buffer: ^6.0.3 - cross-fetch: 3.1.5 - hash.js: ^1.1.7 - iso-3166-1: ^2.1.1 - json-bigint: ^1.0.0 - uuid: ^8.3.2 - checksum: 26cce68b496f9799359666d7ddf430c659c0250e16a96c8bfe1956162f737517896a9ab9e0c9a7a3a17117ee6d763a91cbcde20891c76d87444f67299a37c8c6 - languageName: node - linkType: hard - "@concordium/node-sdk@6.3.0, @concordium/node-sdk@workspace:packages/nodejs": version: 0.0.0-use.local resolution: "@concordium/node-sdk@workspace:packages/nodejs" dependencies: - "@concordium/common-sdk": 6.4.1 + "@concordium/common-sdk": 6.4.2 "@grpc/grpc-js": ^1.3.4 "@noble/ed25519": ^1.7.1 "@protobuf-ts/grpc-transport": ^2.8.2 From 6b8795e862bcb8239e289b96205a1547d7b1f934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 28 Apr 2023 13:58:03 +0200 Subject: [PATCH 6/8] Added example of building account signer from file --- examples/buildAccountSigner.ts | 52 ++++++++++++++++++++++++++++++++++ examples/package.json | 3 +- examples/tsconfig.eslint.json | 4 +-- 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 examples/buildAccountSigner.ts diff --git a/examples/buildAccountSigner.ts b/examples/buildAccountSigner.ts new file mode 100644 index 000000000..96375a716 --- /dev/null +++ b/examples/buildAccountSigner.ts @@ -0,0 +1,52 @@ +import meow from 'meow'; +import fs from 'fs'; +import path from 'path'; +import { + AccountAddress, + signMessage, + buildAccountSigner, +} from '@concordium/node-sdk'; + +const cli = meow( + ` + Usage + $ yarn ts-node [options] + + Required + --keyFile, -f A file containing the private key(s) of an account, which must be a supported format (f.x. a private key export from a Concordium wallet) + + Options + --help, -h Displays this message +`, + { + importMeta: import.meta, + flags: { + keyFile: { + type: 'string', + alias: 'f', + isRequired: true, + }, + }, + } +); + +if (cli.flags.h) { + cli.showHelp(); +} + +const file = fs.readFileSync(path.resolve(process.cwd(), cli.flags.keyFile)); +const contents = JSON.parse(file.toString('utf8')); + +try { + const signer = buildAccountSigner(contents); + + signMessage( + new AccountAddress( + '3eP94feEdmhYiPC1333F9VoV31KGMswonuHk5tqmZrzf761zK5' + ), + 'test', + signer + ).then(console.log); +} catch { + console.error('File passed does not conform to a supported JSON format'); +} diff --git a/examples/package.json b/examples/package.json index e7052cdf2..022d3400c 100644 --- a/examples/package.json +++ b/examples/package.json @@ -20,6 +20,7 @@ "lint": "eslint . --cache --ext .ts,.tsx --max-warnings 0", "lint-fix": "yarn lint --fix; exit 0", "build": "tsc", - "build-dev": "tsc" + "build-dev": "tsc", + "run-example": "node --experimental-specifier-resolution=node --loader ts-node/esm" } } diff --git a/examples/tsconfig.eslint.json b/examples/tsconfig.eslint.json index 78cd7ad3a..7a0b4296a 100644 --- a/examples/tsconfig.eslint.json +++ b/examples/tsconfig.eslint.json @@ -1,7 +1,5 @@ { "include": [ - "client/**/*", - "common/**/*", + "./**/*.ts", ] } - From 07a35846113746efc185df8e0ca5c340d0f7604f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Tue, 2 May 2023 09:06:18 +0200 Subject: [PATCH 7/8] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Søren Hjort <87635671+shjortConcordium@users.noreply.github.com> --- examples/buildAccountSigner.ts | 2 +- packages/common/src/signHelpers.ts | 6 +++--- packages/common/src/types.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/buildAccountSigner.ts b/examples/buildAccountSigner.ts index 96375a716..6b81d78d8 100644 --- a/examples/buildAccountSigner.ts +++ b/examples/buildAccountSigner.ts @@ -13,7 +13,7 @@ const cli = meow( $ yarn ts-node [options] Required - --keyFile, -f A file containing the private key(s) of an account, which must be a supported format (f.x. a private key export from a Concordium wallet) + --keyFile, -f A file containing the private key(s) of an account, which must be a supported format (e.g. a private key export from a Concordium wallet) Options --help, -h Displays this message diff --git a/packages/common/src/signHelpers.ts b/packages/common/src/signHelpers.ts index 71e4438cb..c21fe99b4 100644 --- a/packages/common/src/signHelpers.ts +++ b/packages/common/src/signHelpers.ts @@ -27,14 +27,14 @@ export interface AccountSigner { */ sign(digest: Buffer): Promise; /** - * Amount of signatures created + * Returns the amount of signatures that the signer produces */ getSignatureCount(): bigint; } const getSignature = async ( digest: Buffer, - privateKey: string + privateKey: HexString ): Promise => Buffer.from(await ed.sign(digest, privateKey)).toString('hex'); @@ -46,7 +46,7 @@ const getSignature = async ( * * @returns {AccountSigner} an `AccountSigner` which creates a signature using the first credentials first keypair */ -export function buildBasicAccountSigner(privateKey: string): AccountSigner { +export function buildBasicAccountSigner(privateKey: HexString): AccountSigner { return { getSignatureCount() { return 1n; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 5678db066..c8d290a11 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -739,7 +739,7 @@ export interface WalletExportFormat { value: { accountKeys: AccountKeys; address: Base58String; - credentials: Record; + credentials: Record; }; } From 365f18ceaa0f6901eafb8a1b86c3d01351d070b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Tue, 2 May 2023 09:15:07 +0200 Subject: [PATCH 8/8] PR feedback --- packages/common/README.md | 1 + packages/common/src/signHelpers.ts | 12 +++--------- packages/common/test/signHelpers.test.ts | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/common/README.md b/packages/common/README.md index 04c6a5030..9dfa01fbb 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -24,6 +24,7 @@ This package is the shared library for the nodejs and web SDK's. - [Deserialize a receive function's return value](#deserialize-a-receive-functions-return-value) - [Deserialize a function's error](#deserialize-a-functions-error) - [Deserialize a transaction](#deserialize-a-transaction) + - [Creating an AccountSigner](#creating-an-accountsigner) - [Sign an account transaction](#sign-an-account-transaction) - [Sign a message](#sign-a-message) - [Identity proofs](#identity-proofs) diff --git a/packages/common/src/signHelpers.ts b/packages/common/src/signHelpers.ts index c21fe99b4..5b64a69b7 100644 --- a/packages/common/src/signHelpers.ts +++ b/packages/common/src/signHelpers.ts @@ -13,6 +13,7 @@ import * as ed from '@noble/ed25519'; import { Buffer } from 'buffer/'; import { AccountAddress } from './types/accountAddress'; import { sha256 } from './hash'; +import { mapRecord } from './util'; /** * A structure to use for creating signatures on a given digest. @@ -82,15 +83,8 @@ const getKeys = ( ? value.value.accountKeys : value.accountKeys; - return Object.entries(keys).reduce( - (acc, [k, v]) => ({ - ...acc, - [k]: Object.entries(v.keys).reduce( - (a, e) => ({ ...a, [e[0]]: e[1].signKey }), - {} - ), - }), - {} + return mapRecord(keys, (credKeys) => + mapRecord(credKeys.keys, (keyPair) => keyPair.signKey) ); }; diff --git a/packages/common/test/signHelpers.test.ts b/packages/common/test/signHelpers.test.ts index ca57f383a..7f7d96d99 100644 --- a/packages/common/test/signHelpers.test.ts +++ b/packages/common/test/signHelpers.test.ts @@ -171,7 +171,7 @@ test('verifyMessageSignature returns false on the incorrect signature', async () }); testEachMessageType( - '[%o] verifyMessageSignature returns false on not enough signatures', + '[%o] verifyMessageSignature returns false on not enough signatures on specific credential', async (message) => { const signature = await verifyMessageSignature( message,