diff --git a/packages/accounts/jest.config.js b/packages/accounts/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/accounts/jest.config.js +++ b/packages/accounts/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/accounts/test/providers.test.js b/packages/accounts/test/providers.test.js index 982d23016..8d335c676 100644 --- a/packages/accounts/test/providers.test.js +++ b/packages/accounts/test/providers.test.js @@ -2,7 +2,7 @@ const base58 = require('bs58'); const testUtils = require('./test-utils'); const { KeyPair } = require('@near-js/crypto'); -let ERRORS_JSON = require('../../utils/lib/errors/error_messages.json'); +let ERRORS_JSON = require('@near-js/utils/lib/errors/error_messages.json'); jest.setTimeout(60000); diff --git a/packages/biometric-ed25519/jest.config.js b/packages/biometric-ed25519/jest.config.js new file mode 100644 index 000000000..030333402 --- /dev/null +++ b/packages/biometric-ed25519/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], +}; diff --git a/packages/crypto/jest.config.js b/packages/crypto/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/crypto/jest.config.js +++ b/packages/crypto/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/crypto/src/key_pair_ed25519.ts b/packages/crypto/src/key_pair_ed25519.ts index 34bae522c..7d6f1e807 100644 --- a/packages/crypto/src/key_pair_ed25519.ts +++ b/packages/crypto/src/key_pair_ed25519.ts @@ -24,7 +24,7 @@ export class KeyPairEd25519 extends KeyPairBase { super(); const decoded = baseDecode(extendedSecretKey); const secretKey = new Uint8Array(decoded.slice(0, KeySize.SECRET_KEY)); - const publicKey = ed25519.getPublicKey(secretKey); + const publicKey = ed25519.getPublicKey(new Uint8Array(secretKey)); this.publicKey = new PublicKey({ keyType: KeyType.ED25519, data: publicKey }); this.secretKey = baseEncode(secretKey); this.extendedSecretKey = extendedSecretKey; diff --git a/packages/crypto/test/key_pair.test.js b/packages/crypto/test/key_pair.test.js index b93cb13e4..8c28052d6 100644 --- a/packages/crypto/test/key_pair.test.js +++ b/packages/crypto/test/key_pair.test.js @@ -1,8 +1,10 @@ const { baseEncode } = require('@near-js/utils'); const { sha256 } = require('@noble/hashes/sha256'); - const { KeyPair, KeyPairEd25519, PublicKey } = require('../lib'); +const { TextEncoder } = require('util'); +global.TextEncoder = TextEncoder; + test('test sign and verify', async () => { const keyPair = new KeyPairEd25519('26x56YPzPDro5t2smQfGcYAPy3j7R2jB2NUb7xKbAGK23B6x4WNQPh3twb6oDksFov5X8ts5CtntUNbpQpAKFdbR'); expect(keyPair.publicKey.toString()).toEqual('ed25519:AYWv9RAN1hpSQA4p1DLhCNnpnNXwxhfH9qeHN8B4nJ59'); diff --git a/packages/keystores-browser/jest.config.js b/packages/keystores-browser/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/keystores-browser/jest.config.js +++ b/packages/keystores-browser/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/keystores-browser/test/browser_keystore.test.js b/packages/keystores-browser/test/browser_keystore.test.js index cb71965ca..7bdf34206 100644 --- a/packages/keystores-browser/test/browser_keystore.test.js +++ b/packages/keystores-browser/test/browser_keystore.test.js @@ -7,5 +7,5 @@ describe('Browser keystore', () => { ctx.keyStore = new BrowserLocalStorageKeyStore(require('localstorage-memory')); }); - require('./keystore_common').shouldStoreAndRetriveKeys(ctx); + require('./keystore_common').shouldStoreAndRetrieveKeys(ctx); }); diff --git a/packages/keystores-browser/test/keystore_common.js b/packages/keystores-browser/test/keystore_common.js index 40fd26361..9d721f0f2 100644 --- a/packages/keystores-browser/test/keystore_common.js +++ b/packages/keystores-browser/test/keystore_common.js @@ -4,7 +4,7 @@ const NETWORK_ID_SINGLE_KEY = 'singlekeynetworkid'; const ACCOUNT_ID_SINGLE_KEY = 'singlekey_accountid'; const KEYPAIR_SINGLE_KEY = new KeyPairEd25519('2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw'); -module.exports.shouldStoreAndRetriveKeys = ctx => { +module.exports.shouldStoreAndRetrieveKeys = ctx => { beforeEach(async () => { await ctx.keyStore.clear(); await ctx.keyStore.setKey(NETWORK_ID_SINGLE_KEY, ACCOUNT_ID_SINGLE_KEY, KEYPAIR_SINGLE_KEY); diff --git a/packages/keystores-node/jest.config.js b/packages/keystores-node/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/keystores-node/jest.config.js +++ b/packages/keystores-node/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/keystores-node/test/unencrypted_file_system_keystore.test.js b/packages/keystores-node/test/unencrypted_file_system_keystore.test.node.js similarity index 100% rename from packages/keystores-node/test/unencrypted_file_system_keystore.test.js rename to packages/keystores-node/test/unencrypted_file_system_keystore.test.node.js diff --git a/packages/keystores/jest.config.js b/packages/keystores/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/keystores/jest.config.js +++ b/packages/keystores/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/keystores/test/in_memory_keystore.test.js b/packages/keystores/test/in_memory_keystore.test.js index 52a0eda48..8cf3efeaa 100644 --- a/packages/keystores/test/in_memory_keystore.test.js +++ b/packages/keystores/test/in_memory_keystore.test.js @@ -7,5 +7,5 @@ describe('In-memory keystore', () => { ctx.keyStore = new InMemoryKeyStore(); }); - require('./keystore_common').shouldStoreAndRetriveKeys(ctx); + require('./keystore_common').shouldStoreAndRetrieveKeys(ctx); }); diff --git a/packages/keystores/test/keystore_common.js b/packages/keystores/test/keystore_common.js index 40fd26361..9d721f0f2 100644 --- a/packages/keystores/test/keystore_common.js +++ b/packages/keystores/test/keystore_common.js @@ -4,7 +4,7 @@ const NETWORK_ID_SINGLE_KEY = 'singlekeynetworkid'; const ACCOUNT_ID_SINGLE_KEY = 'singlekey_accountid'; const KEYPAIR_SINGLE_KEY = new KeyPairEd25519('2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw'); -module.exports.shouldStoreAndRetriveKeys = ctx => { +module.exports.shouldStoreAndRetrieveKeys = ctx => { beforeEach(async () => { await ctx.keyStore.clear(); await ctx.keyStore.setKey(NETWORK_ID_SINGLE_KEY, ACCOUNT_ID_SINGLE_KEY, KEYPAIR_SINGLE_KEY); diff --git a/packages/keystores/test/merge_keystore.test.js b/packages/keystores/test/merge_keystore.test.js index 76ee7ac2e..5a7d22ca3 100644 --- a/packages/keystores/test/merge_keystore.test.js +++ b/packages/keystores/test/merge_keystore.test.js @@ -31,5 +31,5 @@ describe('Merge keystore', () => { expect(await ctx.stores[1].getAccounts('network')).toHaveLength(0); }); - require('./keystore_common').shouldStoreAndRetriveKeys(ctx); + require('./keystore_common').shouldStoreAndRetrieveKeys(ctx); }); diff --git a/packages/near-api-js/test/config.js b/packages/near-api-js/test/config.js index 7bbf1bfc0..23f994a1d 100644 --- a/packages/near-api-js/test/config.js +++ b/packages/near-api-js/test/config.js @@ -3,49 +3,50 @@ const fs = require('fs'); let worker; module.exports = async function getConfig(env) { switch (env) { - case 'production': - case 'mainnet': - return { - networkId: 'mainnet', - nodeUrl: 'https://rpc.mainnet.near.org', - walletUrl: 'https://wallet.near.org', - helperUrl: 'https://helper.mainnet.near.org', - }; - case 'development': - case 'testnet': - return { - networkId: 'default', - nodeUrl: 'https://rpc.testnet.near.org', - walletUrl: 'https://wallet.testnet.near.org', - helperUrl: 'https://helper.testnet.near.org', - masterAccount: 'test.near', - }; - case 'betanet': - return { - networkId: 'betanet', - nodeUrl: 'https://rpc.betanet.near.org', - walletUrl: 'https://wallet.betanet.near.org', - helperUrl: 'https://helper.betanet.near.org', - }; - case 'local': - return { - networkId: 'local', - nodeUrl: 'http://localhost:3030', - keyPath: `${process.env.HOME}/.near/validator_key.json`, - walletUrl: 'http://localhost:4000/wallet', - }; - case 'test': - case 'ci': - if (!worker) worker = await Worker.init(); - const keyFile = fs.readFileSync(`${worker.rootAccount.manager.config.homeDir}/validator_key.json`); - const keyPair = JSON.parse(keyFile.toString()); - return { - networkId: worker.config.network, - nodeUrl: worker.manager.config.rpcAddr, - masterAccount: worker.rootAccount._accountId, - secretKey: keyPair.secret_key || keyPair.private_key - }; - default: - throw Error(`Unconfigured environment '${env}'. Can be configured in src/config.js.`); + case 'production': + case 'mainnet': + return { + networkId: 'mainnet', + nodeUrl: 'https://rpc.mainnet.near.org', + walletUrl: 'https://wallet.near.org', + helperUrl: 'https://helper.mainnet.near.org', + }; + case 'development': + case 'testnet': + return { + networkId: 'default', + nodeUrl: 'https://rpc.testnet.near.org', + walletUrl: 'https://wallet.testnet.near.org', + helperUrl: 'https://helper.testnet.near.org', + masterAccount: 'test.near', + }; + case 'betanet': + return { + networkId: 'betanet', + nodeUrl: 'https://rpc.betanet.near.org', + walletUrl: 'https://wallet.betanet.near.org', + helperUrl: 'https://helper.betanet.near.org', + }; + case 'local': + return { + networkId: 'local', + nodeUrl: 'http://localhost:3030', + keyPath: `${process.env.HOME}/.near/validator_key.json`, + walletUrl: 'http://localhost:4000/wallet', + }; + case 'test': + case 'ci': { + if (!worker) worker = await Worker.init(); + const keyFile = fs.readFileSync(`${worker.rootAccount.manager.config.homeDir}/validator_key.json`); + const keyPair = JSON.parse(keyFile.toString()); + return { + networkId: worker.config.network, + nodeUrl: worker.manager.config.rpcAddr, + masterAccount: worker.rootAccount._accountId, + secretKey: keyPair.secret_key || keyPair.private_key + }; + } + default: + throw Error(`Unconfigured environment '${env}'. Can be configured in src/config.js.`); } }; diff --git a/packages/providers/jest.config.js b/packages/providers/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/providers/jest.config.js +++ b/packages/providers/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/providers/test/providers.test.js b/packages/providers/test/providers.test.js index ad8f3ae0f..93e33cce2 100644 --- a/packages/providers/test/providers.test.js +++ b/packages/providers/test/providers.test.js @@ -2,6 +2,9 @@ const { getTransactionLastResult } = require('@near-js/utils'); const { Worker } = require('near-workspaces'); const { JsonRpcProvider, FailoverRpcProvider } = require('../lib'); +const { TextEncoder } = require('util'); +global.TextEncoder = TextEncoder; + jest.setTimeout(20000); ['json provider', 'fallback provider'].forEach((name) => { diff --git a/packages/signers/jest.config.js b/packages/signers/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/signers/jest.config.js +++ b/packages/signers/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/signers/test/signer.test.js b/packages/signers/test/signer.test.js index f037b8889..13e7fac67 100644 --- a/packages/signers/test/signer.test.js +++ b/packages/signers/test/signer.test.js @@ -2,6 +2,9 @@ const { InMemoryKeyStore } = require('@near-js/keystores'); const { InMemorySigner } = require('../lib'); +const { TextEncoder } = require('util'); +global.TextEncoder = TextEncoder; + test('test no key', async() => { const signer = new InMemorySigner(new InMemoryKeyStore()); await expect(signer.signMessage('message', 'user', 'network')) diff --git a/packages/transactions/jest.config.js b/packages/transactions/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/transactions/jest.config.js +++ b/packages/transactions/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/wallet-account/jest.config.js b/packages/wallet-account/jest.config.js index 749b7fcb2..030333402 100644 --- a/packages/wallet-account/jest.config.js +++ b/packages/wallet-account/jest.config.js @@ -1,5 +1,22 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true + preset: "ts-jest", + collectCoverage: true, + projects: [ + { + displayName: "dom", + testEnvironment: "jsdom", // runs tests in a browser-like environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.dom.js?(x)", + ], + }, + { + displayName: "node", + testEnvironment: "node", // runs tests in a Node.js environment + testMatch: [ + "**/test/**/*.test.js?(x)", + "**/test/**/*.test.node.js?(x)", + ], + }, + ], }; diff --git a/packages/wallet-account/test/wallet_account.js b/packages/wallet-account/test/wallet_account.js new file mode 100644 index 000000000..bfa9302d3 --- /dev/null +++ b/packages/wallet-account/test/wallet_account.js @@ -0,0 +1,454 @@ +const { KeyPair, PublicKey } = require('@near-js/crypto'); +const { InMemoryKeyStore } = require('@near-js/keystores'); +const { InMemorySigner } = require('@near-js/signers'); +const { baseDecode } = require('@near-js/utils'); +const { actionCreators, createTransaction, SCHEMA } = require('@near-js/transactions'); +const BN = require('bn.js'); +const { deserialize } = require('borsh'); +const localStorage = require('localstorage-memory'); +const url = require('url'); + +const { WalletConnection } = require('../lib/wallet_account'); + +const { functionCall, transfer } = actionCreators; +// If an access key has itself as receiverId and method permission add_request_and_confirm, then it is being used in a wallet with multisig contract: https://github.com/near/core-contracts/blob/671c05f09abecabe7a7e58efe942550a35fc3292/multisig/src/lib.rs#L149-L153 +const MULTISIG_HAS_METHOD = 'add_request_and_confirm'; + +let lastRedirectUrl; +let lastTransaction; +global.window = { + localStorage +}; +global.document = { + title: 'documentTitle' +}; + +let history; +let nearFake; +let walletConnection; +let keyStore = new InMemoryKeyStore(); +module.exports.createTransactions = () => { + beforeEach(() => { + keyStore.clear(); + nearFake = { + config: { + networkId: 'networkId', + contractName: 'contractId', + walletUrl: 'http://example.com/wallet', + }, + connection: { + networkId: 'networkId', + signer: new InMemorySigner(keyStore) + }, + account() { + return { + state() {} + }; + } + }; + lastRedirectUrl = null; + history = []; + Object.assign(global.window, { + location: { + href: 'http://example.com/location', + assign(url) { + lastRedirectUrl = url; + } + }, + history: { + replaceState: (state, title, url) => history.push([state, title, url]) + } + }); + walletConnection = new WalletConnection(nearFake, ''); + }); + + it('not signed in by default', () => { + expect(walletConnection.isSignedIn()).not.toBeTruthy(); + }); + + it('throws if non string appKeyPrefix', () => { + expect(() => new WalletConnection(nearFake)).toThrow(/appKeyPrefix/); + expect(() => new WalletConnection(nearFake, 1)).toThrow(/appKeyPrefix/); + expect(() => new WalletConnection(nearFake, null)).toThrow(/appKeyPrefix/); + expect(() => new WalletConnection(nearFake, undefined)).toThrow(/appKeyPrefix/); + }); + // const windowValueBefore = global.window; + + // beforeEach(() => { + // global.window = undefined; + // keyStore.clear(); + // }); + + // afterEach(() => { + // global.window = windowValueBefore; + // }); + + // it('does not throw on instantiation', () => { + // expect(() => new WalletConnection(nearFake, '')).not.toThrowError(); + // }); + + // it('throws if non string appKeyPrefix in server context', () => { + // expect(() => new WalletConnection(nearFake)).toThrow(/appKeyPrefix/); + // expect(() => new WalletConnection(nearFake, 1)).toThrow(/appKeyPrefix/); + // expect(() => new WalletConnection(nearFake, null)).toThrow(/appKeyPrefix/); + // expect(() => new WalletConnection(nearFake, undefined)).toThrow(/appKeyPrefix/); + // }); + + // it('returns an empty string as accountId', () => { + // const serverWalletConnection = new WalletConnection(nearFake, ''); + // expect(serverWalletConnection.getAccountId()).toEqual(''); + // }); + + // it('returns false as isSignedIn', () => { + // const serverWalletConnection = new WalletConnection(nearFake, ''); + // expect(serverWalletConnection.isSignedIn()).toEqual(false); + // }); + + // it('throws explicit error when calling other methods on the instance', () => { + // const serverWalletConnection = new WalletConnection(nearFake, ''); + // expect(() => serverWalletConnection.requestSignIn('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); + // expect(() => serverWalletConnection.requestSignInUrl('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); + // expect(() => serverWalletConnection.requestSignTransactions('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); + // expect(() => serverWalletConnection.requestSignTransactionsUrl('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); + // }); + + // it('can access other props on the instance', () => { + // const serverWalletConnection = new WalletConnection(nearFake, ''); + // expect(serverWalletConnection['randomValue']).toEqual(undefined); + // }); + // }); + + // describe('can request sign in', () => { + // beforeEach(() => keyStore.clear()); + + // it('V2', () => { + // return walletConnection.requestSignIn({ + // contractId: 'signInContract', + // successUrl: 'http://example.com/success', + // failureUrl: 'http://example.com/fail' + // }); + // }); + + // afterEach(async () => { + // let accounts = await keyStore.getAccounts('networkId'); + // expect(accounts).toHaveLength(1); + // expect(accounts[0]).toMatch(/^pending_key.+/); + // expect(url.parse(lastRedirectUrl, true)).toMatchObject({ + // protocol: 'http:', + // host: 'example.com', + // query: { + // contract_id: 'signInContract', + // success_url: 'http://example.com/success', + // failure_url: 'http://example.com/fail', + // public_key: (await keyStore.getKey('networkId', accounts[0])).publicKey.toString() + // } + // }); + // }); + // }); + + // it('can request sign in with methodNames', async () => { + // await walletConnection.requestSignIn({ + // contractId: 'signInContract', + // methodNames: ['hello', 'goodbye'], + // successUrl: 'http://example.com/success', + // failureUrl: 'http://example.com/fail' + // }); + + // let accounts = await keyStore.getAccounts('networkId'); + // expect(accounts).toHaveLength(1); + // expect(accounts[0]).toMatch(/^pending_key.+/); + // expect(url.parse(lastRedirectUrl, true)).toMatchObject({ + // protocol: 'http:', + // host: 'example.com', + // query: { + // contract_id: 'signInContract', + // methodNames: ['hello', 'goodbye'], + // success_url: 'http://example.com/success', + // failure_url: 'http://example.com/fail', + // public_key: (await keyStore.getKey('networkId', accounts[0])).publicKey.toString() + // } + // }); + // }); + + // it('can complete sign in', async () => { + // const keyPair = KeyPair.fromRandom('ed25519'); + // global.window.location.href = `http://example.com/location?account_id=near.account&public_key=${keyPair.publicKey}`; + // await keyStore.setKey('networkId', 'pending_key' + keyPair.publicKey, keyPair); + + // await walletConnection._completeSignInWithAccessKey(); + + // expect(await keyStore.getKey('networkId', 'near.account')).toEqual(keyPair); + // expect(localStorage.getItem('contractId_wallet_auth_key')); + // expect(history.slice(1)).toEqual([ + // [{}, 'documentTitle', 'http://example.com/location'] + // ]); + // }); + + // it('Promise until complete sign in', async () => { + // const keyPair = KeyPair.fromRandom('ed25519'); + // global.window.location.href = `http://example.com/location?account_id=near2.account&public_key=${keyPair.publicKey}`; + // await keyStore.setKey('networkId', 'pending_key' + keyPair.publicKey, keyPair); + + // const newWalletConn = new WalletConnection(nearFake, 'promise_on_complete_signin'); + + // expect(newWalletConn.isSignedIn()).toEqual(false); + // expect(await newWalletConn.isSignedInAsync()).toEqual(true); + // expect(await keyStore.getKey('networkId', 'near2.account')).toEqual(keyPair); + // expect(localStorage.getItem('promise_on_complete_signin_wallet_auth_key')); + // expect(history).toEqual([ + // [{}, 'documentTitle', 'http://example.com/location'] + // ]); + // }); + + const BLOCK_HASH = '244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM'; + + function setupWalletConnectionForSigning({ allKeys, accountAccessKeys }) { + walletConnection._authData = { + allKeys: allKeys, + accountId: 'signer.near' + }; + nearFake.connection.provider = { + query(params) { + if (params.request_type === 'view_account' && params.account_id === 'signer.near') { + return { }; + } + if (params.request_type === 'view_access_key_list' && params.account_id === 'signer.near') { + return { keys: accountAccessKeys }; + } + if (params.request_type === 'view_access_key' && params.account_id === 'signer.near') { + for (let accessKey of accountAccessKeys) { + if (accessKey.public_key === params.public_key) { + return accessKey; + } + } + } + fail(`Unexpected query: ${JSON.stringify(params)}`); + }, + sendTransaction(signedTransaction) { + lastTransaction = signedTransaction; + return { + transaction_outcome: { outcome: { logs: [] } }, + receipts_outcome: [] + }; + }, + block() { + return { + header: { + hash: BLOCK_HASH + } + }; + } + }; + } + // const keyPair = KeyPair.fromRandom('ed25519'); + // let transactions; + // beforeEach(() => { + // setupWalletConnectionForSigning({ + // allKeys: [ 'no_such_access_key', keyPair.publicKey.toString() ], + // accountAccessKeys: [{ + // access_key: { + // nonce: 1, + // permission: 'FullAccess' + // }, + // public_key: keyPair.publicKey.toString() + // }] + // }); + // }); + + // it('V2', async() => { + // try { + // await walletConnection.account().signAndSendTransaction({ + // receiverId: 'receiver.near', + // actions: [transfer(1)], + // walletCallbackUrl: 'http://callback.com/callback' + // }); + // fail('expected to throw'); + // } catch (e) { + // expect(e.message).toEqual('Failed to redirect to sign transaction'); + // } + // transactions = parseTransactionsFromUrl(lastRedirectUrl, 'http://callback.com/callback'); + // }); + + // afterEach(() => { + // expect(transactions).toHaveLength(1); + // expect(transactions[0]).toMatchObject({ + // signerId: 'signer.near', + // // nonce: new BN(2) + // receiverId: 'receiver.near', + // actions: [{ + // transfer: { + // // deposit: new BN(1) + // } + // }] + // }); + // expect(transactions[0].nonce.toString()).toEqual('2'); + // expect(transactions[0].actions[0].transfer.deposit.toString()).toEqual('1'); + // expect(Buffer.from(transactions[0].publicKey.data)).toEqual(Buffer.from(keyPair.publicKey.data)); + // }); + // }); + + // describe('requests transaction signing automatically when function call has attached deposit', () => { + // beforeEach(async() => { + // const localKeyPair = KeyPair.fromRandom('ed25519'); + // const walletKeyPair = KeyPair.fromRandom('ed25519'); + // setupWalletConnectionForSigning({ + // allKeys: [ walletKeyPair.publicKey.toString() ], + // accountAccessKeys: [{ + // access_key: { + // nonce: 1, + // permission: { + // FunctionCall: { + // allowance: '1000000000', + // receiver_id: 'receiver.near', + // method_names: [] + // } + // } + // }, + // public_key: localKeyPair.publicKey.toString() + // }, { + // access_key: { + // nonce: 1, + // permission: 'FullAccess' + // }, + // public_key: walletKeyPair.publicKey.toString() + // }] + // }); + // await keyStore.setKey('networkId', 'signer.near', localKeyPair); + // }); + + // it('V2', async() => { + // try { + // await walletConnection.account().signAndSendTransaction({ + // receiverId: 'receiver.near', + // actions: [functionCall('someMethod', new Uint8Array(), new BN('1'), new BN('1'))], + // walletCallbackUrl: 'http://example.com/after', + // walletMeta: 'someStuff' + // }); + // fail('expected to throw'); + // } catch (e) { + // expect(e.message).toEqual('Failed to redirect to sign transaction'); + // } + + // const transactions = parseTransactionsFromUrl(lastRedirectUrl, 'http://example.com/after'); + // expect(transactions).toHaveLength(1); + // }); + // }); + + describe('requests transaction signing with 2fa access key', () => { + beforeEach(async () => { + let localKeyPair = KeyPair.fromRandom('ed25519'); + let walletKeyPair = KeyPair.fromRandom('ed25519'); + setupWalletConnectionForSigning({ + allKeys: [ walletKeyPair.publicKey.toString() ], + accountAccessKeys: [{ + access_key: { + nonce: 1, + permission: { + FunctionCall: { + allowance: '1000000000', + receiver_id: 'signer.near', + method_names: [MULTISIG_HAS_METHOD] + } + } + }, + public_key: localKeyPair.publicKey.toString() + }] + }); + await keyStore.setKey('networkId', 'signer.near', localKeyPair); + }); + + it('V2', async () => { + try { + const res = await walletConnection.account().signAndSendTransaction({ + receiverId: 'receiver.near', + actions: [functionCall('someMethod', new Uint8Array(), new BN('1'), new BN('1'))] + }); + + // multisig access key is accepted res is object representing transaction, populated upon wallet redirect to app + expect(res).toHaveProperty('transaction_outcome'); + expect(res).toHaveProperty('receipts_outcome'); + } catch (e) { + fail('expected transaction outcome'); + } + }); + }); + + describe('fails requests transaction signing without 2fa access key', () => { + beforeEach(async () => { + const localKeyPair = KeyPair.fromRandom('ed25519'); + const walletKeyPair = KeyPair.fromRandom('ed25519'); + setupWalletConnectionForSigning({ + allKeys: [ walletKeyPair.publicKey.toString() ], + accountAccessKeys: [{ + access_key: { + nonce: 1, + permission: { + FunctionCall: { + allowance: '1000000000', + receiver_id: 'signer.near', + method_names: ['not_a_valid_2fa_method'] + } + } + }, + public_key: localKeyPair.publicKey.toString() + }] + }); + await keyStore.setKey('networkId', 'signer.near', localKeyPair); + }); + + it('V2', () => { + return expect( + walletConnection.account().signAndSendTransaction({ + receiverId: 'receiver.near', + actions: [functionCall('someMethod', new Uint8Array(), new BN('1'), new BN('1'))] + }) + ).rejects.toThrow('Cannot find matching key for transaction sent to receiver.near'); + }); + }); + + describe('can sign transaction locally when function call has no attached deposit', () => { + beforeEach(async () => { + const localKeyPair = KeyPair.fromRandom('ed25519'); + setupWalletConnectionForSigning({ + allKeys: [ /* no keys in wallet needed */ ], + accountAccessKeys: [{ + access_key: { + nonce: 1, + permission: { + FunctionCall: { + allowance: '1000000000', + receiver_id: 'receiver.near', + method_names: [] + } + } + }, + public_key: localKeyPair.publicKey.toString() + }] + }); + await keyStore.setKey('networkId', 'signer.near', localKeyPair); + }); + + it.each([ + functionCall('someMethod', new Uint8Array(), new BN('1'), new BN('0')), + functionCall('someMethod', new Uint8Array(), new BN('1')), + functionCall('someMethod', new Uint8Array()) + ])('V2', async (functionCall) => { + await walletConnection.account().signAndSendTransaction({ + receiverId: 'receiver.near', + actions: [ functionCall ] + }); + // NOTE: Transaction gets signed without wallet in this test + expect(lastTransaction).toMatchObject({ + transaction: { + receiverId: 'receiver.near', + signerId: 'signer.near', + actions: [{ + functionCall: { + methodName: 'someMethod', + } + }] + } + }); + }); + }); +} \ No newline at end of file diff --git a/packages/wallet-account/test/wallet_account.test.js b/packages/wallet-account/test/wallet_account.test.js index c64c16d49..dd723aa02 100644 --- a/packages/wallet-account/test/wallet_account.test.js +++ b/packages/wallet-account/test/wallet_account.test.js @@ -1,521 +1 @@ -const { KeyPair, PublicKey } = require('@near-js/crypto'); -const { InMemoryKeyStore } = require('@near-js/keystores'); -const { InMemorySigner } = require('@near-js/signers'); -const { baseDecode } = require('@near-js/utils'); -const { actionCreators, createTransaction, SCHEMA } = require('@near-js/transactions'); -const { deserialize } = require('borsh'); -const localStorage = require('localstorage-memory'); -const url = require('url'); - -const { WalletConnection } = require('../lib/wallet_account'); - -const { functionCall, transfer } = actionCreators; - -// If an access key has itself as receiverId and method permission add_request_and_confirm, then it is being used in a wallet with multisig contract: https://github.com/near/core-contracts/blob/671c05f09abecabe7a7e58efe942550a35fc3292/multisig/src/lib.rs#L149-L153 -const MULTISIG_HAS_METHOD = 'add_request_and_confirm'; - -let lastRedirectUrl; -let lastTransaction; -global.window = { - localStorage -}; -global.document = { - title: 'documentTitle' -}; - -let history; -let nearFake; -let walletConnection; -let keyStore = new InMemoryKeyStore(); -beforeEach(() => { - keyStore.clear(); - nearFake = { - config: { - networkId: 'networkId', - contractName: 'contractId', - walletUrl: 'http://example.com/wallet', - }, - connection: { - networkId: 'networkId', - signer: new InMemorySigner(keyStore) - }, - account() { - return { - state() {} - }; - } - }; - lastRedirectUrl = null; - history = []; - Object.assign(global.window, { - location: { - href: 'http://example.com/location', - assign(url) { - lastRedirectUrl = url; - } - }, - history: { - replaceState: (state, title, url) => history.push([state, title, url]) - } - }); - walletConnection = new WalletConnection(nearFake, ''); -}); - -it('not signed in by default', () => { - expect(walletConnection.isSignedIn()).not.toBeTruthy(); -}); - -it('throws if non string appKeyPrefix', () => { - expect(() => new WalletConnection(nearFake)).toThrow(/appKeyPrefix/); - expect(() => new WalletConnection(nearFake, 1)).toThrow(/appKeyPrefix/); - expect(() => new WalletConnection(nearFake, null)).toThrow(/appKeyPrefix/); - expect(() => new WalletConnection(nearFake, undefined)).toThrow(/appKeyPrefix/); -}); - -describe('fails gracefully on the server side (without window)', () => { - const windowValueBefore = global.window; - - beforeEach(() => { - global.window = undefined; - keyStore.clear(); - }); - - afterEach(() => { - global.window = windowValueBefore; - }); - - it('does not throw on instantiation', () => { - expect(() => new WalletConnection(nearFake, '')).not.toThrowError(); - }); - - it('throws if non string appKeyPrefix in server context', () => { - expect(() => new WalletConnection(nearFake)).toThrow(/appKeyPrefix/); - expect(() => new WalletConnection(nearFake, 1)).toThrow(/appKeyPrefix/); - expect(() => new WalletConnection(nearFake, null)).toThrow(/appKeyPrefix/); - expect(() => new WalletConnection(nearFake, undefined)).toThrow(/appKeyPrefix/); - }); - - it('returns an empty string as accountId', () => { - const serverWalletConnection = new WalletConnection(nearFake, ''); - expect(serverWalletConnection.getAccountId()).toEqual(''); - }); - - it('returns false as isSignedIn', () => { - const serverWalletConnection = new WalletConnection(nearFake, ''); - expect(serverWalletConnection.isSignedIn()).toEqual(false); - }); - - it('throws explicit error when calling other methods on the instance', () => { - const serverWalletConnection = new WalletConnection(nearFake, ''); - expect(() => serverWalletConnection.requestSignIn('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); - expect(() => serverWalletConnection.requestSignInUrl('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); - expect(() => serverWalletConnection.requestSignTransactions('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); - expect(() => serverWalletConnection.requestSignTransactionsUrl('signInContract', 'signInTitle', 'http://example.com/success', 'http://example.com/fail')).toThrow(/please ensure you are using WalletConnection on the browser/); - }); - - it('can access other props on the instance', () => { - const serverWalletConnection = new WalletConnection(nearFake, ''); - expect(serverWalletConnection['randomValue']).toEqual(undefined); - }); -}); - -describe('can request sign in', () => { - beforeEach(() => keyStore.clear()); - - it('V2', () => { - return walletConnection.requestSignIn({ - contractId: 'signInContract', - successUrl: 'http://example.com/success', - failureUrl: 'http://example.com/fail' - }); - }); - - afterEach(async () => { - let accounts = await keyStore.getAccounts('networkId'); - expect(accounts).toHaveLength(1); - expect(accounts[0]).toMatch(/^pending_key.+/); - expect(url.parse(lastRedirectUrl, true)).toMatchObject({ - protocol: 'http:', - host: 'example.com', - query: { - contract_id: 'signInContract', - success_url: 'http://example.com/success', - failure_url: 'http://example.com/fail', - public_key: (await keyStore.getKey('networkId', accounts[0])).publicKey.toString() - } - }); - }); -}); - -it('can request sign in with methodNames', async () => { - await walletConnection.requestSignIn({ - contractId: 'signInContract', - methodNames: ['hello', 'goodbye'], - successUrl: 'http://example.com/success', - failureUrl: 'http://example.com/fail' - }); - - let accounts = await keyStore.getAccounts('networkId'); - expect(accounts).toHaveLength(1); - expect(accounts[0]).toMatch(/^pending_key.+/); - expect(url.parse(lastRedirectUrl, true)).toMatchObject({ - protocol: 'http:', - host: 'example.com', - query: { - contract_id: 'signInContract', - methodNames: ['hello', 'goodbye'], - success_url: 'http://example.com/success', - failure_url: 'http://example.com/fail', - public_key: (await keyStore.getKey('networkId', accounts[0])).publicKey.toString() - } - }); -}); - -it('can complete sign in', async () => { - const keyPair = KeyPair.fromRandom('ed25519'); - global.window.location.href = `http://example.com/location?account_id=near.account&public_key=${keyPair.publicKey}`; - await keyStore.setKey('networkId', 'pending_key' + keyPair.publicKey, keyPair); - - await walletConnection._completeSignInWithAccessKey(); - - expect(await keyStore.getKey('networkId', 'near.account')).toEqual(keyPair); - expect(localStorage.getItem('contractId_wallet_auth_key')); - expect(history.slice(1)).toEqual([ - [{}, 'documentTitle', 'http://example.com/location'] - ]); -}); - -it('Promise until complete sign in', async () => { - const keyPair = KeyPair.fromRandom('ed25519'); - global.window.location.href = `http://example.com/location?account_id=near2.account&public_key=${keyPair.publicKey}`; - await keyStore.setKey('networkId', 'pending_key' + keyPair.publicKey, keyPair); - - const newWalletConn = new WalletConnection(nearFake, 'promise_on_complete_signin'); - - expect(newWalletConn.isSignedIn()).toEqual(false); - expect(await newWalletConn.isSignedInAsync()).toEqual(true); - expect(await keyStore.getKey('networkId', 'near2.account')).toEqual(keyPair); - expect(localStorage.getItem('promise_on_complete_signin_wallet_auth_key')); - expect(history).toEqual([ - [{}, 'documentTitle', 'http://example.com/location'] - ]); -}); - -const BLOCK_HASH = '244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM'; -const blockHash = baseDecode(BLOCK_HASH); -function createTransferTx() { - const actions = [ - transfer(1), - ]; - return createTransaction( - 'test.near', - PublicKey.fromString('Anu7LYDfpLtkP7E16LT9imXF694BdQaa9ufVkQiwTQxC'), - 'whatever.near', - 1, - actions, - blockHash); -} - -describe('can request transaction signing', () => { - it('V1', async () => { - await walletConnection.requestSignTransactions({ - transactions: [createTransferTx()], - callbackUrl: 'http://example.com/callback' - }); - - expect(url.parse(lastRedirectUrl, true)).toMatchObject({ - protocol: 'http:', - host: 'example.com', - query: { - callbackUrl: 'http://example.com/callback', - transactions: 'CQAAAHRlc3QubmVhcgCRez0mjUtY9/7BsVC9aNab4+5dTMOYVeNBU4Rlu3eGDQEAAAAAAAAADQAAAHdoYXRldmVyLm5lYXIPpHP9JpAd8pa+atxMxN800EDvokNSJLaYaRDmMML+9gEAAAADAQAAAAAAAAAAAAAAAAAAAA==' - } - }); - }); - - it('V2', async () => { - await walletConnection.requestSignTransactions({ - transactions: [createTransferTx()], - meta: 'something', - callbackUrl: 'http://example.com/after' - }); - - expect(url.parse(lastRedirectUrl, true)).toMatchObject({ - protocol: 'http:', - host: 'example.com', - query: { - meta: 'something', - callbackUrl: 'http://example.com/after', - transactions: 'CQAAAHRlc3QubmVhcgCRez0mjUtY9/7BsVC9aNab4+5dTMOYVeNBU4Rlu3eGDQEAAAAAAAAADQAAAHdoYXRldmVyLm5lYXIPpHP9JpAd8pa+atxMxN800EDvokNSJLaYaRDmMML+9gEAAAADAQAAAAAAAAAAAAAAAAAAAA==' - } - }); - }); -}); - -function parseTransactionsFromUrl(urlToParse, callbackUrl = 'http://example.com/location') { - const parsedUrl = url.parse(urlToParse, true); - expect(parsedUrl).toMatchObject({ - protocol: 'http:', - host: 'example.com', - query: { - callbackUrl - } - }); - const transactions = parsedUrl.query.transactions.split(',') - .map(txBase64 => deserialize( - SCHEMA.Transaction, - Buffer.from(txBase64, 'base64'))); - return transactions; -} - -function setupWalletConnectionForSigning({ allKeys, accountAccessKeys }) { - walletConnection._authData = { - allKeys: allKeys, - accountId: 'signer.near' - }; - nearFake.connection.provider = { - query(params) { - if (params.request_type === 'view_account' && params.account_id === 'signer.near') { - return { }; - } - if (params.request_type === 'view_access_key_list' && params.account_id === 'signer.near') { - return { keys: accountAccessKeys }; - } - if (params.request_type === 'view_access_key' && params.account_id === 'signer.near') { - for (let accessKey of accountAccessKeys) { - if (accessKey.public_key === params.public_key) { - return accessKey; - } - } - } - fail(`Unexpected query: ${JSON.stringify(params)}`); - }, - sendTransaction(signedTransaction) { - lastTransaction = signedTransaction; - return { - transaction_outcome: { outcome: { logs: [] } }, - receipts_outcome: [] - }; - }, - block() { - return { - header: { - hash: BLOCK_HASH - } - }; - } - }; -} - -describe('requests transaction signing automatically when there is no local key', () => { - const keyPair = KeyPair.fromRandom('ed25519'); - let transactions; - beforeEach(() => { - setupWalletConnectionForSigning({ - allKeys: [ 'no_such_access_key', keyPair.publicKey.toString() ], - accountAccessKeys: [{ - access_key: { - nonce: 1, - permission: 'FullAccess' - }, - public_key: keyPair.publicKey.toString() - }] - }); - }); - - it('V2', async() => { - try { - await walletConnection.account().signAndSendTransaction({ - receiverId: 'receiver.near', - actions: [transfer(1)], - walletCallbackUrl: 'http://callback.com/callback' - }); - fail('expected to throw'); - } catch (e) { - expect(e.message).toEqual('Failed to redirect to sign transaction'); - } - transactions = parseTransactionsFromUrl(lastRedirectUrl, 'http://callback.com/callback'); - }); - - afterEach(() => { - expect(transactions).toHaveLength(1); - expect(transactions[0]).toMatchObject({ - signerId: 'signer.near', - // nonce: BigInt(2) - receiverId: 'receiver.near', - actions: [{ - transfer: { - // deposit: BigInt(1) - } - }] - }); - expect(transactions[0].nonce.toString()).toEqual('2'); - expect(transactions[0].actions[0].transfer.deposit.toString()).toEqual('1'); - expect(Buffer.from(transactions[0].publicKey.data)).toEqual(Buffer.from(keyPair.publicKey.data)); - }); -}); - -describe('requests transaction signing automatically when function call has attached deposit', () => { - beforeEach(async() => { - const localKeyPair = KeyPair.fromRandom('ed25519'); - const walletKeyPair = KeyPair.fromRandom('ed25519'); - setupWalletConnectionForSigning({ - allKeys: [ walletKeyPair.publicKey.toString() ], - accountAccessKeys: [{ - access_key: { - nonce: 1, - permission: { - FunctionCall: { - allowance: '1000000000', - receiver_id: 'receiver.near', - method_names: [] - } - } - }, - public_key: localKeyPair.publicKey.toString() - }, { - access_key: { - nonce: 1, - permission: 'FullAccess' - }, - public_key: walletKeyPair.publicKey.toString() - }] - }); - await keyStore.setKey('networkId', 'signer.near', localKeyPair); - }); - - it('V2', async() => { - try { - await walletConnection.account().signAndSendTransaction({ - receiverId: 'receiver.near', - actions: [functionCall('someMethod', new Uint8Array(), BigInt('1'), BigInt('1'))], - walletCallbackUrl: 'http://example.com/after', - walletMeta: 'someStuff' - }); - fail('expected to throw'); - } catch (e) { - expect(e.message).toEqual('Failed to redirect to sign transaction'); - } - - const transactions = parseTransactionsFromUrl(lastRedirectUrl, 'http://example.com/after'); - expect(transactions).toHaveLength(1); - }); -}); - -describe('requests transaction signing with 2fa access key', () => { - beforeEach(async () => { - let localKeyPair = KeyPair.fromRandom('ed25519'); - let walletKeyPair = KeyPair.fromRandom('ed25519'); - setupWalletConnectionForSigning({ - allKeys: [ walletKeyPair.publicKey.toString() ], - accountAccessKeys: [{ - access_key: { - nonce: 1, - permission: { - FunctionCall: { - allowance: '1000000000', - receiver_id: 'signer.near', - method_names: [MULTISIG_HAS_METHOD] - } - } - }, - public_key: localKeyPair.publicKey.toString() - }] - }); - await keyStore.setKey('networkId', 'signer.near', localKeyPair); - }); - - it('V2', async () => { - try { - const res = await walletConnection.account().signAndSendTransaction({ - receiverId: 'receiver.near', - actions: [functionCall('someMethod', new Uint8Array(), BigInt('1'), BigInt('1'))] - }); - - // multisig access key is accepted res is object representing transaction, populated upon wallet redirect to app - expect(res).toHaveProperty('transaction_outcome'); - expect(res).toHaveProperty('receipts_outcome'); - } catch (e) { - fail('expected transaction outcome'); - } - }); -}); - -describe('fails requests transaction signing without 2fa access key', () => { - beforeEach(async () => { - const localKeyPair = KeyPair.fromRandom('ed25519'); - const walletKeyPair = KeyPair.fromRandom('ed25519'); - setupWalletConnectionForSigning({ - allKeys: [ walletKeyPair.publicKey.toString() ], - accountAccessKeys: [{ - access_key: { - nonce: 1, - permission: { - FunctionCall: { - allowance: '1000000000', - receiver_id: 'signer.near', - method_names: ['not_a_valid_2fa_method'] - } - } - }, - public_key: localKeyPair.publicKey.toString() - }] - }); - await keyStore.setKey('networkId', 'signer.near', localKeyPair); - }); - - it('V2', () => { - return expect( - walletConnection.account().signAndSendTransaction({ - receiverId: 'receiver.near', - actions: [functionCall('someMethod', new Uint8Array(), BigInt('1'), BigInt('1'))] - }) - ).rejects.toThrow('Cannot find matching key for transaction sent to receiver.near'); - }); -}); - -describe('can sign transaction locally when function call has no attached deposit', () => { - beforeEach(async () => { - const localKeyPair = KeyPair.fromRandom('ed25519'); - setupWalletConnectionForSigning({ - allKeys: [ /* no keys in wallet needed */ ], - accountAccessKeys: [{ - access_key: { - nonce: 1, - permission: { - FunctionCall: { - allowance: '1000000000', - receiver_id: 'receiver.near', - method_names: [] - } - } - }, - public_key: localKeyPair.publicKey.toString() - }] - }); - await keyStore.setKey('networkId', 'signer.near', localKeyPair); - }); - - it.each([ - functionCall('someMethod', new Uint8Array(), BigInt('1'), BigInt('0')), - functionCall('someMethod', new Uint8Array(), BigInt('1')), - functionCall('someMethod', new Uint8Array()) - ])('V2', async (functionCall) => { - await walletConnection.account().signAndSendTransaction({ - receiverId: 'receiver.near', - actions: [ functionCall ] - }); - // NOTE: Transaction gets signed without wallet in this test - expect(lastTransaction).toMatchObject({ - transaction: { - receiverId: 'receiver.near', - signerId: 'signer.near', - actions: [{ - functionCall: { - methodName: 'someMethod', - } - }] - } - }); - }); -}); +require('./wallet_account').createTransactions(); diff --git a/packages/wallet-account/test/wallet_accounts.test.node.js b/packages/wallet-account/test/wallet_accounts.test.node.js new file mode 100644 index 000000000..d7d12ba78 --- /dev/null +++ b/packages/wallet-account/test/wallet_accounts.test.node.js @@ -0,0 +1,505 @@ +const { KeyPair, PublicKey } = require("@near-js/crypto"); +const { InMemoryKeyStore } = require("@near-js/keystores"); +const { InMemorySigner } = require("@near-js/signers"); +const { baseDecode } = require("@near-js/utils"); +const { + actionCreators, + createTransaction, + SCHEMA, +} = require("@near-js/transactions"); +const BN = require("bn.js"); +const { deserialize } = require("borsh"); +const localStorage = require("localstorage-memory"); +const url = require("url"); + +const { WalletConnection } = require("../lib/wallet_account"); + +const { functionCall, transfer } = actionCreators; + +let lastRedirectUrl; +let lastTransaction; +global.window = { + localStorage +}; +global.document = { + title: 'documentTitle' +}; + +let history; +let nearFake; +let walletConnection; +let keyStore = new InMemoryKeyStore(); +describe("Wallet account tests", () => { + beforeEach(() => { + keyStore.clear(); + nearFake = { + config: { + networkId: 'networkId', + contractName: 'contractId', + walletUrl: 'http://example.com/wallet', + }, + connection: { + networkId: 'networkId', + signer: new InMemorySigner(keyStore) + }, + account() { + return { + state() {} + }; + } + }; + lastRedirectUrl = null; + history = []; + Object.assign(global.window, { + location: { + href: 'http://example.com/location', + assign(url) { + lastRedirectUrl = url; + } + }, + history: { + replaceState: (state, title, url) => history.push([state, title, url]) + } + }); + walletConnection = new WalletConnection(nearFake, ''); + }); + + describe("fails gracefully on the server side (without window)", () => { + const windowValueBefore = global.window; + + beforeEach(() => { + global.window = undefined; + keyStore.clear(); + }); + + afterEach(() => { + global.window = windowValueBefore; + }); + + it("does not throw on instantiation", () => { + expect(() => new WalletConnection(nearFake, "")).not.toThrowError(); + }); + + it("throws if non string appKeyPrefix in server context", () => { + expect(() => new WalletConnection(nearFake)).toThrow( + /appKeyPrefix/ + ); + expect(() => new WalletConnection(nearFake, 1)).toThrow( + /appKeyPrefix/ + ); + expect(() => new WalletConnection(nearFake, null)).toThrow( + /appKeyPrefix/ + ); + expect(() => new WalletConnection(nearFake, undefined)).toThrow( + /appKeyPrefix/ + ); + }); + + it("returns an empty string as accountId", () => { + const serverWalletConnection = new WalletConnection(nearFake, ""); + expect(serverWalletConnection.getAccountId()).toEqual(""); + }); + + it("returns false as isSignedIn", () => { + const serverWalletConnection = new WalletConnection(nearFake, ""); + expect(serverWalletConnection.isSignedIn()).toEqual(false); + }); + + it("throws explicit error when calling other methods on the instance", () => { + const serverWalletConnection = new WalletConnection(nearFake, ""); + expect(() => + serverWalletConnection.requestSignIn( + "signInContract", + "signInTitle", + "http://example.com/success", + "http://example.com/fail" + ) + ).toThrow( + /please ensure you are using WalletConnection on the browser/ + ); + expect(() => + serverWalletConnection.requestSignInUrl( + "signInContract", + "signInTitle", + "http://example.com/success", + "http://example.com/fail" + ) + ).toThrow( + /please ensure you are using WalletConnection on the browser/ + ); + expect(() => + serverWalletConnection.requestSignTransactions( + "signInContract", + "signInTitle", + "http://example.com/success", + "http://example.com/fail" + ) + ).toThrow( + /please ensure you are using WalletConnection on the browser/ + ); + expect(() => + serverWalletConnection.requestSignTransactionsUrl( + "signInContract", + "signInTitle", + "http://example.com/success", + "http://example.com/fail" + ) + ).toThrow( + /please ensure you are using WalletConnection on the browser/ + ); + }); + + it("can access other props on the instance", () => { + const serverWalletConnection = new WalletConnection(nearFake, ""); + expect(serverWalletConnection["randomValue"]).toEqual(undefined); + }); + }); + + describe("can request sign in", () => { + beforeEach(() => keyStore.clear()); + + it("V2", () => { + return walletConnection.requestSignIn({ + contractId: "signInContract", + successUrl: "http://example.com/success", + failureUrl: "http://example.com/fail", + }); + }); + + afterEach(async () => { + let accounts = await keyStore.getAccounts("networkId"); + expect(accounts).toHaveLength(1); + expect(accounts[0]).toMatch(/^pending_key.+/); + expect(url.parse(lastRedirectUrl, true)).toMatchObject({ + protocol: "http:", + host: "example.com", + query: { + contract_id: "signInContract", + success_url: "http://example.com/success", + failure_url: "http://example.com/fail", + public_key: ( + await keyStore.getKey("networkId", accounts[0]) + ).publicKey.toString(), + }, + }); + }); + }); + + it("can request sign in with methodNames", async () => { + await walletConnection.requestSignIn({ + contractId: "signInContract", + methodNames: ["hello", "goodbye"], + successUrl: "http://example.com/success", + failureUrl: "http://example.com/fail", + }); + + let accounts = await keyStore.getAccounts("networkId"); + expect(accounts).toHaveLength(1); + expect(accounts[0]).toMatch(/^pending_key.+/); + expect(url.parse(lastRedirectUrl, true)).toMatchObject({ + protocol: "http:", + host: "example.com", + query: { + contract_id: "signInContract", + methodNames: ["hello", "goodbye"], + success_url: "http://example.com/success", + failure_url: "http://example.com/fail", + public_key: ( + await keyStore.getKey("networkId", accounts[0]) + ).publicKey.toString(), + }, + }); + }); + + it("can complete sign in", async () => { + const keyPair = KeyPair.fromRandom("ed25519"); + global.window.location.href = `http://example.com/location?account_id=near.account&public_key=${keyPair.publicKey}`; + await keyStore.setKey( + "networkId", + "pending_key" + keyPair.publicKey, + keyPair + ); + + await walletConnection._completeSignInWithAccessKey(); + + expect(await keyStore.getKey("networkId", "near.account")).toEqual( + keyPair + ); + expect(localStorage.getItem("contractId_wallet_auth_key")); + expect(history.slice(1)).toEqual([ + [{}, "documentTitle", "http://example.com/location"], + ]); + }); + + it("Promise until complete sign in", async () => { + const keyPair = KeyPair.fromRandom("ed25519"); + global.window.location.href = `http://example.com/location?account_id=near2.account&public_key=${keyPair.publicKey}`; + await keyStore.setKey( + "networkId", + "pending_key" + keyPair.publicKey, + keyPair + ); + + const newWalletConn = new WalletConnection( + nearFake, + "promise_on_complete_signin" + ); + + expect(newWalletConn.isSignedIn()).toEqual(false); + expect(await newWalletConn.isSignedInAsync()).toEqual(true); + expect(await keyStore.getKey("networkId", "near2.account")).toEqual( + keyPair + ); + expect( + localStorage.getItem("promise_on_complete_signin_wallet_auth_key") + ); + expect(history).toEqual([ + [{}, "documentTitle", "http://example.com/location"], + ]); + }); + + const BLOCK_HASH = "244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM"; + const blockHash = baseDecode(BLOCK_HASH); + function createTransferTx() { + const actions = [transfer(1)]; + return createTransaction( + "test.near", + PublicKey.fromString( + "Anu7LYDfpLtkP7E16LT9imXF694BdQaa9ufVkQiwTQxC" + ), + "whatever.near", + 1, + actions, + blockHash + ); + } + + describe("can request transaction signing", () => { + it("V1", async () => { + await walletConnection.requestSignTransactions({ + transactions: [createTransferTx()], + callbackUrl: "http://example.com/callback", + }); + + expect(url.parse(lastRedirectUrl, true)).toMatchObject({ + protocol: "http:", + host: "example.com", + query: { + callbackUrl: "http://example.com/callback", + transactions: + "CQAAAHRlc3QubmVhcgCRez0mjUtY9/7BsVC9aNab4+5dTMOYVeNBU4Rlu3eGDQEAAAAAAAAADQAAAHdoYXRldmVyLm5lYXIPpHP9JpAd8pa+atxMxN800EDvokNSJLaYaRDmMML+9gEAAAADAQAAAAAAAAAAAAAAAAAAAA==", + }, + }); + }); + + it("V2", async () => { + await walletConnection.requestSignTransactions({ + transactions: [createTransferTx()], + meta: "something", + callbackUrl: "http://example.com/after", + }); + + expect(url.parse(lastRedirectUrl, true)).toMatchObject({ + protocol: "http:", + host: "example.com", + query: { + meta: "something", + callbackUrl: "http://example.com/after", + transactions: + "CQAAAHRlc3QubmVhcgCRez0mjUtY9/7BsVC9aNab4+5dTMOYVeNBU4Rlu3eGDQEAAAAAAAAADQAAAHdoYXRldmVyLm5lYXIPpHP9JpAd8pa+atxMxN800EDvokNSJLaYaRDmMML+9gEAAAADAQAAAAAAAAAAAAAAAAAAAA==", + }, + }); + }); + }); + + function parseTransactionsFromUrl( + urlToParse, + callbackUrl = "http://example.com/location" + ) { + const parsedUrl = url.parse(urlToParse, true); + expect(parsedUrl).toMatchObject({ + protocol: "http:", + host: "example.com", + query: { + callbackUrl, + }, + }); + const transactions = parsedUrl.query.transactions + .split(",") + .map((txBase64) => + deserialize(SCHEMA.Transaction, Buffer.from(txBase64, "base64")) + ); + return transactions; + } + + function setupWalletConnectionForSigning({ allKeys, accountAccessKeys }) { + walletConnection._authData = { + allKeys: allKeys, + accountId: "signer.near", + }; + nearFake.connection.provider = { + query(params) { + if ( + params.request_type === "view_account" && + params.account_id === "signer.near" + ) { + return {}; + } + if ( + params.request_type === "view_access_key_list" && + params.account_id === "signer.near" + ) { + return { keys: accountAccessKeys }; + } + if ( + params.request_type === "view_access_key" && + params.account_id === "signer.near" + ) { + for (let accessKey of accountAccessKeys) { + if (accessKey.public_key === params.public_key) { + return accessKey; + } + } + } + fail(`Unexpected query: ${JSON.stringify(params)}`); + }, + sendTransaction(signedTransaction) { + lastTransaction = signedTransaction; + return { + transaction_outcome: { outcome: { logs: [] } }, + receipts_outcome: [], + }; + }, + block() { + return { + header: { + hash: BLOCK_HASH, + }, + }; + }, + }; + } + + describe("requests transaction signing automatically when there is no local key", () => { + const keyPair = KeyPair.fromRandom("ed25519"); + let transactions; + beforeEach(() => { + setupWalletConnectionForSigning({ + allKeys: ["no_such_access_key", keyPair.publicKey.toString()], + accountAccessKeys: [ + { + access_key: { + nonce: 1, + permission: "FullAccess", + }, + public_key: keyPair.publicKey.toString(), + }, + ], + }); + }); + + it("V2", async () => { + try { + await walletConnection.account().signAndSendTransaction({ + receiverId: "receiver.near", + actions: [transfer(1)], + walletCallbackUrl: "http://callback.com/callback", + }); + fail("expected to throw"); + } catch (e) { + expect(e.message).toEqual( + "Failed to redirect to sign transaction" + ); + } + transactions = parseTransactionsFromUrl( + lastRedirectUrl, + "http://callback.com/callback" + ); + }); + + afterEach(() => { + expect(transactions).toHaveLength(1); + expect(transactions[0]).toMatchObject({ + signerId: "signer.near", + // nonce: new BN(2) + receiverId: "receiver.near", + actions: [ + { + transfer: { + // deposit: new BN(1) + }, + }, + ], + }); + expect(transactions[0].nonce.toString()).toEqual("2"); + expect( + transactions[0].actions[0].transfer.deposit.toString() + ).toEqual("1"); + expect(Buffer.from(transactions[0].publicKey.data)).toEqual( + Buffer.from(keyPair.publicKey.data) + ); + }); + }); + + describe("requests transaction signing automatically when function call has attached deposit", () => { + beforeEach(async () => { + const localKeyPair = KeyPair.fromRandom("ed25519"); + const walletKeyPair = KeyPair.fromRandom("ed25519"); + setupWalletConnectionForSigning({ + allKeys: [walletKeyPair.publicKey.toString()], + accountAccessKeys: [ + { + access_key: { + nonce: 1, + permission: { + FunctionCall: { + allowance: "1000000000", + receiver_id: "receiver.near", + method_names: [], + }, + }, + }, + public_key: localKeyPair.publicKey.toString(), + }, + { + access_key: { + nonce: 1, + permission: "FullAccess", + }, + public_key: walletKeyPair.publicKey.toString(), + }, + ], + }); + await keyStore.setKey("networkId", "signer.near", localKeyPair); + }); + + it("V2", async () => { + try { + await walletConnection.account().signAndSendTransaction({ + receiverId: "receiver.near", + actions: [ + functionCall( + "someMethod", + new Uint8Array(), + new BN("1"), + new BN("1") + ), + ], + walletCallbackUrl: "http://example.com/after", + walletMeta: "someStuff", + }); + fail("expected to throw"); + } catch (e) { + expect(e.message).toEqual( + "Failed to redirect to sign transaction" + ); + } + + const transactions = parseTransactionsFromUrl( + lastRedirectUrl, + "http://example.com/after" + ); + expect(transactions).toHaveLength(1); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9b33208c..f7e31df34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: '@near-js/transactions': link:../transactions chalk: 4.1.1 homedir: 0.6.0 - near-api-js: link:../near-api-js + near-api-js: 4.0.0 packages/crypto: specifiers: @@ -1510,7 +1510,6 @@ packages: near-abi: 0.1.1 transitivePeerDependencies: - encoding - dev: true /@near-js/crypto/1.2.2: resolution: {integrity: sha512-a/1SICBPg6zo+VGOy+HT448+uHXmlYnP4vwNUdJnmn0yMksi30G1N8BSU5tnP1SX904EASu1pdqSMWslERrCQw==} @@ -1520,28 +1519,24 @@ packages: '@noble/curves': 1.2.0 borsh: 1.0.0 randombytes: 2.1.0 - dev: true /@near-js/keystores-browser/0.0.10: resolution: {integrity: sha512-JX9WbpPnlKQCxEXJJ/AZHj2z2yuR/UnHcUPszz+Q2v1moL5wETBSuwe6jQOfmYZoM+vGU2Vh4fz3V8Ml/Oz3bw==} dependencies: '@near-js/crypto': 1.2.2 '@near-js/keystores': 0.0.10 - dev: true /@near-js/keystores-node/0.0.10: resolution: {integrity: sha512-fM5T+1pe1zHsaBwTW0JzPd5U93u6oGzUheg8Xpj8vEjSvISd9kreY14IQkbtXCtzsTsiIel05mIqHgmg35QNjg==} dependencies: '@near-js/crypto': 1.2.2 '@near-js/keystores': 0.0.10 - dev: true /@near-js/keystores/0.0.10: resolution: {integrity: sha512-rzGMkqY7EcIbUPrcSjK1RJi3dTXusfmZcxg2Jcc9u7VVjEjP/HVnvVaazsYC1la7812VSrDPxqHsZRzWSFxtMA==} dependencies: '@near-js/crypto': 1.2.2 '@near-js/types': 0.1.0 - dev: true /@near-js/providers/0.2.0: resolution: {integrity: sha512-K7RJTkVbn6SD68p/8TkxFjp6jhxNQrkjZ+etQxrBRAMS6kupLTobV+9AfAc1OaORO47X873p4BRhbm9KhCdmZg==} @@ -1555,7 +1550,6 @@ packages: node-fetch: 2.6.7 transitivePeerDependencies: - encoding - dev: true /@near-js/signers/0.1.2: resolution: {integrity: sha512-Echz+ldAFUDGntiBcnCZN4scHBIccz6xVC0Zt9cZu9I4SGKrWga0vNfAwXGVGep8YCEu8MwI9lE2B5n2ouc6kg==} @@ -1563,7 +1557,6 @@ packages: '@near-js/crypto': 1.2.2 '@near-js/keystores': 0.0.10 '@noble/hashes': 1.3.3 - dev: true /@near-js/transactions/1.2.0: resolution: {integrity: sha512-I9UVVPg0HHQUpL17tb9L1HgTMG5+KREI2mNQlvJhF5uE6DrI2tC/O4rQf3HZOUVWpUhOiXSKRfSXpgZq5TbXaw==} @@ -1574,11 +1567,9 @@ packages: '@near-js/utils': 0.2.0 '@noble/hashes': 1.3.3 borsh: 1.0.0 - dev: true /@near-js/types/0.1.0: resolution: {integrity: sha512-uQTB3G7251cKCFhM4poAgypTODb83jrqD5A5B0Nr89TAGbsYM2ozXJyffJpsWedbYhK527Jx/BFgs+Jzf3oO5g==} - dev: true /@near-js/utils/0.2.0: resolution: {integrity: sha512-Ul0NoOiV/vW6hnkYVMwRNbP18hB7mCRkqgSe1a2Qoe+3xFOMnjLVVod3Y1l/QXgp4yNDrLOd6r4PDPI72eKUww==} @@ -1587,7 +1578,6 @@ packages: bs58: 4.0.0 depd: 2.0.0 mustache: 4.0.0 - dev: true /@near-js/wallet-account/1.2.0: resolution: {integrity: sha512-3y0VxE2R2FxAHFJpRkhqIp+ZTc+/n42u3qSH0vzTTihzL2zfGF6z9Y2ArNQ7lQKNyiXd0vsFdAIwVcX5pEfq3A==} @@ -1603,7 +1593,6 @@ packages: borsh: 1.0.0 transitivePeerDependencies: - encoding - dev: true /@noble/curves/1.2.0: resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} @@ -6572,7 +6561,6 @@ packages: node-fetch: 2.6.7 transitivePeerDependencies: - encoding - dev: true /near-hello/0.5.1: resolution: {integrity: sha512-k7S8VFyESWgkKYDso99B4XbxAdo0VX9b3+GAaO5PvMgQjNr/6o09PHRywg/NkBQpf+ZYj7nNpJcyrNJGQsvA3w==}