From 029fcd84b4b01f160173527764f0c840dd247626 Mon Sep 17 00:00:00 2001 From: amirsaran3 Date: Thu, 20 Jul 2023 12:03:59 +0200 Subject: [PATCH 1/8] feat: added multi_contract_keystore --- packages/iframe-rpc/src/iframe-rpc.ts | 8 +- packages/keystores-browser/src/index.ts | 1 + ...ontract_browser_local_storage_key_store.ts | 141 ++++++++++++++++++ .../keystores/src/multi_contract_keystore.ts | 16 ++ 4 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts create mode 100644 packages/keystores/src/multi_contract_keystore.ts diff --git a/packages/iframe-rpc/src/iframe-rpc.ts b/packages/iframe-rpc/src/iframe-rpc.ts index e97bfcfcd3..1bfbace089 100644 --- a/packages/iframe-rpc/src/iframe-rpc.ts +++ b/packages/iframe-rpc/src/iframe-rpc.ts @@ -1,6 +1,6 @@ import EventEmitter from 'events'; -import {IFrameRPCError} from './iframe-rpc-error'; +import { IFrameRPCError } from './iframe-rpc-error'; import { windowReceiver, IMessageEvent, @@ -48,7 +48,7 @@ export class IFrameRPC extends EventEmitter { private createReadyPromise() { return new Promise(resolve => { - const response = {protocolVersion: this.options.protocolVersion || '1.0'}; + const response = { protocolVersion: this.options.protocolVersion || '1.0' }; this.bindMethodHandler('ready', () => { resolve(); @@ -93,7 +93,7 @@ export class IFrameRPC extends EventEmitter { error: err instanceof IFrameRPCError ? err.toResponseError() - : {code: 0, message: err.stack || err.message}, + : { code: 0, message: err.stack || err.message }, } as IRPCResponse)) .then(message => { this.emit('sendResponse', message); @@ -219,7 +219,7 @@ export class IFrameRPC extends EventEmitter { type: 'response', requesterId: this.options.requesterId, id: message.id, - error: {code: 4003, message: `Unknown method name "${message.method}"`}, + error: { code: 4003, message: `Unknown method name "${message.method}"` }, result: null, }); break; diff --git a/packages/keystores-browser/src/index.ts b/packages/keystores-browser/src/index.ts index 2efe7c05b7..dff1cc4029 100644 --- a/packages/keystores-browser/src/index.ts +++ b/packages/keystores-browser/src/index.ts @@ -1 +1,2 @@ export { BrowserLocalStorageKeyStore } from './browser_local_storage_key_store'; +export { MultiContractBrowserLocalStorageKeyStore } from './multi_contract_browser_local_storage_key_store'; diff --git a/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts b/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts new file mode 100644 index 0000000000..74b0b2c9c2 --- /dev/null +++ b/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts @@ -0,0 +1,141 @@ +import { KeyPair } from '@near-js/crypto'; +import { MultiContractKeyStore } from '@near-js/keystores'; + +const LOCAL_STORAGE_KEY_PREFIX = 'near-api-js:keystore:'; + +/** + * This class is used to store keys in the browsers local storage. + * + * @see [https://docs.near.org/docs/develop/front-end/naj-quick-reference#key-store](https://docs.near.org/docs/develop/front-end/naj-quick-reference#key-store) + * @example + * ```js + * import { connect, keyStores } from 'near-api-js'; + * + * const keyStore = new keyStores.MultiContractBrowserLocalStorageKeyStore(); + * const config = { + * keyStore, // instance of MultiContractBrowserLocalStorageKeyStore + * networkId: 'testnet', + * nodeUrl: 'https://rpc.testnet.near.org', + * walletUrl: 'https://wallet.testnet.near.org', + * helperUrl: 'https://helper.testnet.near.org', + * explorerUrl: 'https://explorer.testnet.near.org' + * }; + * + * // inside an async function + * const near = await connect(config) + * ``` + */ +export class MultiContractBrowserLocalStorageKeyStore extends MultiContractKeyStore { + /** @hidden */ + private localStorage: any; + /** @hidden */ + private prefix: string; + + /** + * @param localStorage defaults to window.localStorage + * @param prefix defaults to `near-api-js:keystore:` + */ + constructor(localStorage: any = window.localStorage, prefix = LOCAL_STORAGE_KEY_PREFIX) { + super(); + this.localStorage = localStorage; + this.prefix = prefix; + } + + /** + * Stores a {@link utils/key_pair!KeyPair} in local storage. + * @param networkId The targeted network. (ex. default, betanet, etc…) + * @param accountId The NEAR account tied to the key pair + * @param keyPair The key pair to store in local storage + * @param contractId The contract to store in local storage + */ + async setKey(networkId: string, accountId: string, keyPair: KeyPair, contractId: string): Promise { + this.localStorage.setItem(this.storageKeyForSecretKey(networkId, accountId, contractId), keyPair.toString()); + } + + /** + * Gets a {@link utils/key_pair!KeyPair} from local storage + * @param networkId The targeted network. (ex. default, betanet, etc…) + * @param accountId The NEAR account tied to the key pair + * @param contractId The NEAR contract tied to the key pair + * @returns {Promise} + */ + async getKey(networkId: string, accountId: string, contractId: string): Promise { + const value = this.localStorage.getItem(this.storageKeyForSecretKey(networkId, accountId, contractId)); + if (!value) { + return null; + } + return KeyPair.fromString(value); + } + + /** + * Removes a {@link utils/key_pair!KeyPair} from local storage + * @param networkId The targeted network. (ex. default, betanet, etc…) + * @param accountId The NEAR account tied to the key pair + * @param contractId The NEAR contract tied to the key pair + */ + async removeKey(networkId: string, accountId: string, contractId: string): Promise { + this.localStorage.removeItem(this.storageKeyForSecretKey(networkId, accountId, contractId)); + } + + /** + * Removes all items that start with `prefix` from local storage + */ + async clear(): Promise { + for (const key of this.storageKeys()) { + if (key.startsWith(this.prefix)) { + this.localStorage.removeItem(key); + } + } + } + + /** + * Get the network(s) from local storage + * @returns {Promise} + */ + async getNetworks(): Promise { + const result = new Set(); + for (const key of this.storageKeys()) { + if (key.startsWith(this.prefix)) { + const parts = key.substring(this.prefix.length).split(':'); + result.add(parts[1]); + } + } + return Array.from(result.values()); + } + + /** + * Gets the account(s) from local storage + * @param networkId The targeted network. (ex. default, betanet, etc…) + */ + async getAccounts(networkId: string): Promise { + const result = new Array(); + for (const key of this.storageKeys()) { + if (key.startsWith(this.prefix)) { + const parts = key.substring(this.prefix.length).split(':'); + if (parts[1] === networkId) { + result.push(parts[0]); + } + } + } + return result; + } + + /** + * @hidden + * Helper function to retrieve a local storage key + * @param networkId The targeted network. (ex. default, betanet, etc…) + * @param accountId The NEAR account tied to the storage keythat's sought + * @param contractId The NEAR contract tied to the storage keythat's sought + * @returns {string} An example might be: `near-api-js:keystore:near-friend:default` + */ + private storageKeyForSecretKey(networkId: string, accountId: string, contractId: string): string { + return `${this.prefix}${accountId}:${networkId}:${contractId}`; + } + + /** @hidden */ + private *storageKeys(): IterableIterator { + for (let i = 0; i < this.localStorage.length; i++) { + yield this.localStorage.key(i); + } + } +} diff --git a/packages/keystores/src/multi_contract_keystore.ts b/packages/keystores/src/multi_contract_keystore.ts new file mode 100644 index 0000000000..5dffbd5474 --- /dev/null +++ b/packages/keystores/src/multi_contract_keystore.ts @@ -0,0 +1,16 @@ +import { KeyPair } from '@near-js/crypto'; + +/** + * KeyStores are passed to {@link near!Near} via {@link near!NearConfig} + * and are used by the {@link signer!InMemorySigner} to sign transactions. + * + * @see {@link connect} + */ +export abstract class MultiContractKeyStore { + abstract setKey(networkId: string, accountId: string, keyPair: KeyPair, contractId: string): Promise; + abstract getKey(networkId: string, accountId: string, contractId: string): Promise; + abstract removeKey(networkId: string, accountId: string, contractId: string): Promise; + abstract clear(): Promise; + abstract getNetworks(): Promise; + abstract getAccounts(networkId: string, contractId: string): Promise; +} From 19ddf7414de06826e0030b8dba5ce5711455b242 Mon Sep 17 00:00:00 2001 From: amirsaran3 Date: Thu, 20 Jul 2023 12:08:17 +0200 Subject: [PATCH 2/8] chore: reverted changes --- packages/iframe-rpc/src/iframe-rpc.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/iframe-rpc/src/iframe-rpc.ts b/packages/iframe-rpc/src/iframe-rpc.ts index 1bfbace089..fc5ab17513 100644 --- a/packages/iframe-rpc/src/iframe-rpc.ts +++ b/packages/iframe-rpc/src/iframe-rpc.ts @@ -1,6 +1,6 @@ import EventEmitter from 'events'; -import { IFrameRPCError } from './iframe-rpc-error'; +import {IFrameRPCError} from './iframe-rpc-error'; import { windowReceiver, IMessageEvent, @@ -48,7 +48,7 @@ export class IFrameRPC extends EventEmitter { private createReadyPromise() { return new Promise(resolve => { - const response = { protocolVersion: this.options.protocolVersion || '1.0' }; + const response = {protocolVersion: this.options.protocolVersion || '1.0'}; this.bindMethodHandler('ready', () => { resolve(); @@ -93,7 +93,7 @@ export class IFrameRPC extends EventEmitter { error: err instanceof IFrameRPCError ? err.toResponseError() - : { code: 0, message: err.stack || err.message }, + : {code: 0, message: err.stack || err.message}, } as IRPCResponse)) .then(message => { this.emit('sendResponse', message); @@ -219,7 +219,7 @@ export class IFrameRPC extends EventEmitter { type: 'response', requesterId: this.options.requesterId, id: message.id, - error: { code: 4003, message: `Unknown method name "${message.method}"` }, + error: {code: 4003, message: `Unknown method name "${message.method}"`}, result: null, }); break; @@ -231,4 +231,4 @@ export class IFrameRPC extends EventEmitter { // Ignore } } -} +} \ No newline at end of file From 1ba16da8df6cd312037e4e8ab05fc008ca5238d6 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Mon, 19 Aug 2024 09:14:57 -0700 Subject: [PATCH 3/8] feat: export base class --- packages/keystores/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/keystores/src/index.ts b/packages/keystores/src/index.ts index f095dee7f8..36c3f96339 100644 --- a/packages/keystores/src/index.ts +++ b/packages/keystores/src/index.ts @@ -1,3 +1,4 @@ export { InMemoryKeyStore } from './in_memory_key_store'; export { KeyStore } from './keystore'; export { MergeKeyStore } from './merge_key_store'; +export { MultiContractKeyStore } from './multi_contract_keystore'; From 630da79561d6ff4c57edd15da2583796cbd53dd8 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Wed, 24 Jul 2024 09:33:17 +0300 Subject: [PATCH 4/8] fix: fix var types --- ...ontract_browser_local_storage_key_store.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts b/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts index 74b0b2c9c2..234fd680f9 100644 --- a/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts +++ b/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts @@ -1,4 +1,4 @@ -import { KeyPair } from '@near-js/crypto'; +import { KeyPair, KeyPairString } from '@near-js/crypto'; import { MultiContractKeyStore } from '@near-js/keystores'; const LOCAL_STORAGE_KEY_PREFIX = 'near-api-js:keystore:'; @@ -27,7 +27,7 @@ const LOCAL_STORAGE_KEY_PREFIX = 'near-api-js:keystore:'; */ export class MultiContractBrowserLocalStorageKeyStore extends MultiContractKeyStore { /** @hidden */ - private localStorage: any; + private localStorage: Storage; /** @hidden */ private prefix: string; @@ -64,7 +64,7 @@ export class MultiContractBrowserLocalStorageKeyStore extends MultiContractKeySt if (!value) { return null; } - return KeyPair.fromString(value); + return KeyPair.fromString(value as KeyPairString); } /** @@ -108,7 +108,7 @@ export class MultiContractBrowserLocalStorageKeyStore extends MultiContractKeySt * @param networkId The targeted network. (ex. default, betanet, etc…) */ async getAccounts(networkId: string): Promise { - const result = new Array(); + const result: string[] = []; for (const key of this.storageKeys()) { if (key.startsWith(this.prefix)) { const parts = key.substring(this.prefix.length).split(':'); @@ -120,6 +120,24 @@ export class MultiContractBrowserLocalStorageKeyStore extends MultiContractKeySt return result; } + /** + * Gets the contract(s) from local storage + * @param networkId The targeted network. (ex. default, betanet, etc…) + * @param accountId The targeted account. + */ + async getContracts(networkId: string, accountId: string): Promise { + const result: string[] = []; + for (const key of this.storageKeys()) { + if (key.startsWith(this.prefix)) { + const parts = key.substring(this.prefix.length).split(':'); + if (parts[1] === networkId && parts[0] === accountId) { + result.push(parts[2]); + } + } + } + return result; + } + /** * @hidden * Helper function to retrieve a local storage key @@ -135,7 +153,7 @@ export class MultiContractBrowserLocalStorageKeyStore extends MultiContractKeySt /** @hidden */ private *storageKeys(): IterableIterator { for (let i = 0; i < this.localStorage.length; i++) { - yield this.localStorage.key(i); + yield this.localStorage.key(i) as string; } } } From 528ae0b83ca800a7cd25b32cafe9997fa1a21e29 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Wed, 24 Jul 2024 09:43:35 +0300 Subject: [PATCH 5/8] fix: add missing methid in MultiContractKeyStore --- packages/keystores/src/multi_contract_keystore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/keystores/src/multi_contract_keystore.ts b/packages/keystores/src/multi_contract_keystore.ts index 5dffbd5474..c6eb98d99a 100644 --- a/packages/keystores/src/multi_contract_keystore.ts +++ b/packages/keystores/src/multi_contract_keystore.ts @@ -12,5 +12,6 @@ export abstract class MultiContractKeyStore { abstract removeKey(networkId: string, accountId: string, contractId: string): Promise; abstract clear(): Promise; abstract getNetworks(): Promise; - abstract getAccounts(networkId: string, contractId: string): Promise; + abstract getAccounts(networkId: string): Promise; + abstract getContracts(networkId: string, accountId: string): Promise; } From 99114f765a278e6fa97af4ff5c2ece88eb32a7d1 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Wed, 7 Aug 2024 13:20:20 +0300 Subject: [PATCH 6/8] fix: prefix value --- .../src/multi_contract_browser_local_storage_key_store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts b/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts index 234fd680f9..33cdcb029d 100644 --- a/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts +++ b/packages/keystores-browser/src/multi_contract_browser_local_storage_key_store.ts @@ -38,7 +38,7 @@ export class MultiContractBrowserLocalStorageKeyStore extends MultiContractKeySt constructor(localStorage: any = window.localStorage, prefix = LOCAL_STORAGE_KEY_PREFIX) { super(); this.localStorage = localStorage; - this.prefix = prefix; + this.prefix = prefix || LOCAL_STORAGE_KEY_PREFIX; } /** From 9c7db11c3c5c031a749dd72d5140f58056570e36 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Tue, 23 Jul 2024 16:48:21 +0300 Subject: [PATCH 7/8] feat: add multi_contract_keystore tests --- .changeset/clean-cougars-jump.md | 6 ++ .../test/browser_keystore.test.js | 13 +++- .../multi_contract_browser_keystore_common.js | 64 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 .changeset/clean-cougars-jump.md create mode 100644 packages/keystores-browser/test/multi_contract_browser_keystore_common.js diff --git a/.changeset/clean-cougars-jump.md b/.changeset/clean-cougars-jump.md new file mode 100644 index 0000000000..2185c73d17 --- /dev/null +++ b/.changeset/clean-cougars-jump.md @@ -0,0 +1,6 @@ +--- +"@near-js/keystores": minor +"@near-js/keystores-browser": minor +--- + +Add multi_contract_keystore diff --git a/packages/keystores-browser/test/browser_keystore.test.js b/packages/keystores-browser/test/browser_keystore.test.js index 7bdf34206c..117c8a3ef4 100644 --- a/packages/keystores-browser/test/browser_keystore.test.js +++ b/packages/keystores-browser/test/browser_keystore.test.js @@ -1,4 +1,4 @@ -const { BrowserLocalStorageKeyStore } = require('../lib'); +const { BrowserLocalStorageKeyStore, MultiContractBrowserLocalStorageKeyStore } = require('../lib'); describe('Browser keystore', () => { let ctx = {}; @@ -9,3 +9,14 @@ describe('Browser keystore', () => { require('./keystore_common').shouldStoreAndRetrieveKeys(ctx); }); + + +describe('Browser multi keystore', () => { + let ctx = {}; + + beforeAll(async () => { + ctx.keyStore = new MultiContractBrowserLocalStorageKeyStore(require('localstorage-memory')); + }); + + require('./multi_contract_browser_keystore_common').shouldStoreAndRetrieveKeys(ctx); +}); \ No newline at end of file diff --git a/packages/keystores-browser/test/multi_contract_browser_keystore_common.js b/packages/keystores-browser/test/multi_contract_browser_keystore_common.js new file mode 100644 index 0000000000..b484332e30 --- /dev/null +++ b/packages/keystores-browser/test/multi_contract_browser_keystore_common.js @@ -0,0 +1,64 @@ +const { KeyPairEd25519 } = require('@near-js/crypto'); + +const NETWORK_ID = 'networkid'; +const ACCOUNT_ID = 'accountid'; +const CONTRACT_ID = 'contractid'; +const KEYPAIR = new KeyPairEd25519('2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw'); + +module.exports.shouldStoreAndRetrieveKeys = ctx => { + beforeEach(async () => { + await ctx.keyStore.clear(); + await ctx.keyStore.setKey(NETWORK_ID, ACCOUNT_ID, KEYPAIR, CONTRACT_ID); + }); + + test('Get all keys with empty network returns empty list', async () => { + const emptyList = await ctx.keyStore.getAccounts('emptynetwork'); + expect(emptyList).toEqual([]); + }); + + test('Get all keys with single key in keystore', async () => { + const accountIds = await ctx.keyStore.getAccounts(NETWORK_ID); + expect(accountIds).toEqual([ACCOUNT_ID]); + }); + + test('Get not-existing account', async () => { + expect(await ctx.keyStore.getKey('somenetwork', 'someaccount', 'somecontract')).toBeNull(); + }); + + test('Get account id from a network with single key', async () => { + const key = await ctx.keyStore.getKey(NETWORK_ID, ACCOUNT_ID, CONTRACT_ID); + expect(key).toEqual(KEYPAIR); + }); + + test('Get networks', async () => { + const networks = await ctx.keyStore.getNetworks(); + expect(networks).toEqual([NETWORK_ID]); + }); + + test('Get accounts', async () => { + const accounts = await ctx.keyStore.getAccounts(NETWORK_ID); + expect(accounts).toEqual([ACCOUNT_ID]); + }); + + test('Get contracts', async () => { + const contracts = await ctx.keyStore.getContracts(NETWORK_ID, ACCOUNT_ID); + expect(contracts).toEqual([CONTRACT_ID]); + }); + + test('Add two contracts to account and retrieve them', async () => { + const networkId = "network" + const accountId = "account" + const contract1 = "contract1" + const contract2 = "contract2" + const key1Expected = KeyPairEd25519.fromRandom(); + const key2Expected = KeyPairEd25519.fromRandom(); + await ctx.keyStore.setKey(networkId, accountId, key1Expected, contract1); + await ctx.keyStore.setKey(networkId, accountId, key2Expected, contract2); + const key1 = await ctx.keyStore.getKey(networkId, accountId, contract1); + const key2 = await ctx.keyStore.getKey(networkId, accountId, contract2); + expect(key1).toEqual(key1Expected); + expect(key2).toEqual(key2Expected); + const contractIds = await ctx.keyStore.getContracts(networkId, accountId); + expect(contractIds).toEqual([contract1, contract2]); + }); +}; From ca580b13f3c3802528c4900c1acf4f3b15545f38 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Tue, 20 Aug 2024 08:44:03 +0300 Subject: [PATCH 8/8] fix: fix linter errors --- .../test/multi_contract_browser_keystore_common.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/keystores-browser/test/multi_contract_browser_keystore_common.js b/packages/keystores-browser/test/multi_contract_browser_keystore_common.js index b484332e30..4329544cd5 100644 --- a/packages/keystores-browser/test/multi_contract_browser_keystore_common.js +++ b/packages/keystores-browser/test/multi_contract_browser_keystore_common.js @@ -46,10 +46,10 @@ module.exports.shouldStoreAndRetrieveKeys = ctx => { }); test('Add two contracts to account and retrieve them', async () => { - const networkId = "network" - const accountId = "account" - const contract1 = "contract1" - const contract2 = "contract2" + const networkId = 'network'; + const accountId = 'account'; + const contract1 = 'contract1'; + const contract2 = 'contract2'; const key1Expected = KeyPairEd25519.fromRandom(); const key2Expected = KeyPairEd25519.fromRandom(); await ctx.keyStore.setKey(networkId, accountId, key1Expected, contract1);