Skip to content

Commit

Permalink
Merge pull request #1370 from near/feat/multi-contract-keystore-rebased
Browse files Browse the repository at this point in the history
feat: MultiContractKeystore (rebased)
  • Loading branch information
andy-haynes committed Aug 20, 2024
2 parents 9d86917 + ca580b1 commit 672c37f
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changeset/clean-cougars-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@near-js/keystores": minor
"@near-js/keystores-browser": minor
---

Add multi_contract_keystore
2 changes: 1 addition & 1 deletion packages/iframe-rpc/src/iframe-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,4 @@ export class IFrameRPC extends EventEmitter {
// Ignore
}
}
}
}
1 change: 1 addition & 0 deletions packages/keystores-browser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { BrowserLocalStorageKeyStore } from './browser_local_storage_key_store';
export { MultiContractBrowserLocalStorageKeyStore } from './multi_contract_browser_local_storage_key_store';
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { KeyPair, KeyPairString } 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: Storage;
/** @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 || LOCAL_STORAGE_KEY_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<void> {
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<KeyPair>}
*/
async getKey(networkId: string, accountId: string, contractId: string): Promise<KeyPair | null> {
const value = this.localStorage.getItem(this.storageKeyForSecretKey(networkId, accountId, contractId));
if (!value) {
return null;
}
return KeyPair.fromString(value as KeyPairString);
}

/**
* 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<void> {
this.localStorage.removeItem(this.storageKeyForSecretKey(networkId, accountId, contractId));
}

/**
* Removes all items that start with `prefix` from local storage
*/
async clear(): Promise<void> {
for (const key of this.storageKeys()) {
if (key.startsWith(this.prefix)) {
this.localStorage.removeItem(key);
}
}
}

/**
* Get the network(s) from local storage
* @returns {Promise<string[]>}
*/
async getNetworks(): Promise<string[]> {
const result = new Set<string>();
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<string[]> {
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) {
result.push(parts[0]);
}
}
}
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<string[]> {
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
* @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<string> {
for (let i = 0; i < this.localStorage.length; i++) {
yield this.localStorage.key(i) as string;
}
}
}
13 changes: 12 additions & 1 deletion packages/keystores-browser/test/browser_keystore.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { BrowserLocalStorageKeyStore } = require('../lib');
const { BrowserLocalStorageKeyStore, MultiContractBrowserLocalStorageKeyStore } = require('../lib');

describe('Browser keystore', () => {
let ctx = {};
Expand All @@ -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);
});
Original file line number Diff line number Diff line change
@@ -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]);
});
};
1 change: 1 addition & 0 deletions packages/keystores/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
17 changes: 17 additions & 0 deletions packages/keystores/src/multi_contract_keystore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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<void>;
abstract getKey(networkId: string, accountId: string, contractId: string): Promise<KeyPair | null>;
abstract removeKey(networkId: string, accountId: string, contractId: string): Promise<void>;
abstract clear(): Promise<void>;
abstract getNetworks(): Promise<string[]>;
abstract getAccounts(networkId: string): Promise<string[]>;
abstract getContracts(networkId: string, accountId: string): Promise<string[]>;
}

0 comments on commit 672c37f

Please sign in to comment.