From d7047f1f08534f570048110107f900bfe80fca1a Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Thu, 15 Aug 2019 18:16:14 -0700 Subject: [PATCH] NEP 5 changes --- lib/account.d.ts | 3 +- lib/account.js | 30 +++++++++++----- lib/transaction.d.ts | 30 +++++++++++----- lib/transaction.js | 68 ++++++++++++++++++++++-------------- src.ts/account.ts | 29 +++++++++++----- src.ts/transaction.ts | 78 +++++++++++++++++++++++++++--------------- test/serialize.test.js | 14 ++++---- 7 files changed, 164 insertions(+), 88 deletions(-) diff --git a/lib/account.d.ts b/lib/account.d.ts index a4b1d3c76d..2a9e81a99c 100644 --- a/lib/account.d.ts +++ b/lib/account.d.ts @@ -3,16 +3,15 @@ import { FinalTransactionResult } from './providers/provider'; import { Connection } from './connection'; export interface AccountState { account_id: string; - nonce: number; amount: string; staked: string; - public_keys: Uint8Array[]; code_hash: string; } export declare class Account { readonly connection: Connection; readonly accountId: string; private _state; + private _access_key; private _ready; protected readonly ready: Promise; constructor(connection: Connection, accountId: string); diff --git a/lib/account.js b/lib/account.js index 5244a32f1c..5e02501e89 100644 --- a/lib/account.js +++ b/lib/account.js @@ -1,9 +1,5 @@ 'use strict'; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", { value: true }); -const bn_js_1 = __importDefault(require("bn.js")); const transaction_1 = require("./transaction"); const provider_1 = require("./providers/provider"); const serialize_1 = require("./utils/serialize"); @@ -30,8 +26,14 @@ class Account { this.accountId = accountId; } async fetchState() { - const state = await this.connection.provider.query(`account/${this.accountId}`, ''); - this._state = state; + this._state = await this.connection.provider.query(`account/${this.accountId}`, ''); + try { + const publicKey = await this.connection.signer.getPublicKey(this.accountId, this.connection.networkId); + this._access_key = await this.connection.provider.query(`access_key/${this.accountId}/${publicKey}`, ''); + } + catch { + this._access_key = null; + } } async state() { await this.ready; @@ -58,7 +60,10 @@ class Account { } async signAndSendTransaction(receiverId, actions) { await this.ready; - const [txHash, signedTx] = await transaction_1.signTransaction(receiverId, ++this._state.nonce, actions, this.connection.signer, this.accountId, this.connection.networkId); + if (this._access_key === null) { + throw new Error(`Can not sign transactions, initialize account with available public key in Signer.`); + } + const [txHash, signedTx] = await transaction_1.signTransaction(receiverId, ++this._access_key.nonce, actions, this.connection.signer, this.accountId, this.connection.networkId); let result; try { result = await this.connection.provider.sendTransaction(signedTx); @@ -94,7 +99,7 @@ class Account { return this.signAndSendTransaction(receiverId, [transaction_1.transfer(amount)]); } async createAccount(newAccountId, publicKey, amount) { - const accessKey = transaction_1.createAccessKey(null, "", newAccountId, new bn_js_1.default(0)); + const accessKey = transaction_1.fullAccessKey(); return this.signAndSendTransaction(newAccountId, [transaction_1.createAccount(), transaction_1.transfer(amount), transaction_1.addKey(publicKey, accessKey)]); } async deployContract(data) { @@ -106,8 +111,15 @@ class Account { } return this.signAndSendTransaction(contractId, [transaction_1.functionCall(methodName, Buffer.from(JSON.stringify(args)), gas || DEFAULT_FUNC_CALL_AMOUNT, amount)]); } + // TODO: expand this API to support more options. async addKey(publicKey, contractId, methodName, balanceOwner, amount) { - const accessKey = transaction_1.createAccessKey(contractId ? contractId : null, methodName, balanceOwner, amount); + let accessKey; + if (contractId === null) { + accessKey = transaction_1.fullAccessKey(); + } + else { + accessKey = transaction_1.functionCallAccessKey(contractId, methodName === null ? [] : [methodName], amount); + } return this.signAndSendTransaction(this.accountId, [transaction_1.addKey(publicKey, accessKey)]); } async deleteKey(publicKey) { diff --git a/lib/transaction.d.ts b/lib/transaction.d.ts index 5d538496e6..692c8b8cf2 100644 --- a/lib/transaction.d.ts +++ b/lib/transaction.d.ts @@ -1,15 +1,29 @@ import BN from 'bn.js'; import { Signer } from './signer'; -export declare class Assignable { +declare class Enum { + enum: string; constructor(properties: any); } +declare class Assignable { + constructor(properties: any); +} +export declare class FunctionCallPermission extends Assignable { + allowance?: BN; + receiverId: string; + methodNames: String[]; +} +export declare class FullAccessPermission extends Assignable { +} +export declare class AccessKeyPermission extends Enum { + functionCall: FunctionCallPermission; + fullAccess: FullAccessPermission; +} export declare class AccessKey extends Assignable { - contractId: string; - methodName: Uint8Array; - balanceOwner: string; - amount: BN; + nonce: number; + permission: AccessKeyPermission; } -export declare function createAccessKey(contractId?: string, methodName?: string, balanceOwner?: string, amount?: BN): AccessKey; +export declare function fullAccessKey(): AccessKey; +export declare function functionCallAccessKey(receiverId: string, methodNames: String[], allowance?: BN): AccessKey; export declare class IAction extends Assignable { } declare class CreateAccount extends IAction { @@ -68,8 +82,7 @@ export declare class SignedTransaction extends Assignable { signature: Uint8Array; encode(): Uint8Array; } -export declare class Action { - action: string; +export declare class Action extends Enum { createAccount: CreateAccount; deployContract: DeployContract; functionCall: FunctionCall; @@ -78,7 +91,6 @@ export declare class Action { addKey: AddKey; deleteKey: DeleteKey; deleteAccount: DeleteAccount; - constructor(properties: any); } export declare function signTransaction(receiverId: string, nonce: number, actions: Action[], signer: Signer, accountId?: string, networkId?: string): Promise<[Uint8Array, SignedTransaction]>; export {}; diff --git a/lib/transaction.js b/lib/transaction.js index 65d7b28edf..8633930630 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -4,8 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const js_sha256_1 = __importDefault(require("js-sha256")); -const bn_js_1 = __importDefault(require("bn.js")); const serialize_1 = require("./utils/serialize"); +class Enum { + constructor(properties) { + if (Object.keys(properties).length != 1) { + throw new Error("Enum can only take single value"); + } + Object.keys(properties).map((key) => { + this[key] = properties[key]; + this.enum = key; + }); + } +} class Assignable { constructor(properties) { Object.keys(properties).map((key) => { @@ -13,19 +23,26 @@ class Assignable { }); } } -exports.Assignable = Assignable; +class FunctionCallPermission extends Assignable { +} +exports.FunctionCallPermission = FunctionCallPermission; +class FullAccessPermission extends Assignable { +} +exports.FullAccessPermission = FullAccessPermission; +class AccessKeyPermission extends Enum { +} +exports.AccessKeyPermission = AccessKeyPermission; class AccessKey extends Assignable { } exports.AccessKey = AccessKey; -function createAccessKey(contractId, methodName, balanceOwner, amount) { - return new AccessKey({ - contractId, - methodName, - balanceOwner, - amount: amount || new bn_js_1.default(0), - }); -} -exports.createAccessKey = createAccessKey; +function fullAccessKey() { + return new AccessKey({ nonce: 0, permission: new AccessKeyPermission({ fullAccess: new FullAccessPermission({}) }) }); +} +exports.fullAccessKey = fullAccessKey; +function functionCallAccessKey(receiverId, methodNames, allowance) { + return new AccessKey({ nonce: 0, permission: new AccessKeyPermission({ functionCall: new FunctionCallPermission({ receiverId, allowance, methodNames }) }) }); +} +exports.functionCallAccessKey = functionCallAccessKey; class IAction extends Assignable { } exports.IAction = IAction; @@ -95,16 +112,7 @@ class SignedTransaction extends Assignable { } } exports.SignedTransaction = SignedTransaction; -class Action { - constructor(properties) { - if (Object.keys(properties).length != 1) { - throw new Error("Action can only take single value"); - } - Object.keys(properties).map((key) => { - this[key] = properties[key]; - this.action = key; - }); - } +class Action extends Enum { } exports.Action = Action; const SCHEMA = { @@ -116,12 +124,20 @@ const SCHEMA = { kind: 'struct', fields: [['keyType', 'u8'], ['data', [32]]] }, 'AccessKey': { kind: 'struct', fields: [ - ['amount', 'u128'], - ['balanceOwner', { kind: 'option', type: 'string' }], - ['contractId', { kind: 'option', type: 'string' }], - ['methodName', { kind: 'option', type: ['u8'] }], + ['nonce', 'u64'], + ['permission', AccessKeyPermission], + ] }, + 'AccessKeyPermission': { kind: 'enum', field: 'enum', values: [ + ['functionCall', FunctionCallPermission], + ['fullAccess', FullAccessPermission], + ] }, + 'FunctionCallPermission': { kind: 'struct', fields: [ + ['allowance', { kind: 'option', type: 'u128' }], + ['receiverId', 'string'], + ['methodNames', ['string']], ] }, - 'Action': { kind: 'enum', field: 'action', values: [ + 'FullAccessPermission': { kind: 'struct', fields: [] }, + 'Action': { kind: 'enum', field: 'enum', values: [ ['createAccount', CreateAccount], ['deployContract', DeployContract], ['functionCall', functionCall], diff --git a/src.ts/account.ts b/src.ts/account.ts index 10e89c5d83..4ae8645eea 100644 --- a/src.ts/account.ts +++ b/src.ts/account.ts @@ -2,7 +2,7 @@ import BN from 'bn.js'; import { Action, transfer, createAccount, signTransaction, deployContract, - addKey, functionCall, createAccessKey, deleteKey, stake } from './transaction'; + addKey, functionCall, fullAccessKey, functionCallAccessKey, deleteKey, stake, AccessKey } from './transaction'; import { FinalTransactionResult, FinalTransactionStatus } from './providers/provider'; import { Connection } from './connection'; import { base_encode } from './utils/serialize'; @@ -28,10 +28,8 @@ function sleep(millis: number): Promise { export interface AccountState { account_id: string; - nonce: number; amount: string; staked: string; - public_keys: Uint8Array[]; code_hash: string; } @@ -39,6 +37,7 @@ export class Account { readonly connection: Connection; readonly accountId: string; private _state: AccountState; + private _access_key: AccessKey; private _ready: Promise; protected get ready(): Promise { @@ -51,8 +50,13 @@ export class Account { } async fetchState(): Promise { - const state = await this.connection.provider.query(`account/${this.accountId}`, ''); - this._state = state; + this._state = await this.connection.provider.query(`account/${this.accountId}`, ''); + try { + const publicKey = await this.connection.signer.getPublicKey(this.accountId, this.connection.networkId); + this._access_key = await this.connection.provider.query(`access_key/${this.accountId}/${publicKey}`, ''); + } catch { + this._access_key = null; + } } async state(): Promise { @@ -83,8 +87,11 @@ export class Account { private async signAndSendTransaction(receiverId: string, actions: Array): Promise { await this.ready; + if (this._access_key === null) { + throw new Error(`Can not sign transactions, initialize account with available public key in Signer.`); + } const [txHash, signedTx] = await signTransaction( - receiverId, ++this._state.nonce, actions, this.connection.signer, this.accountId, this.connection.networkId); + receiverId, ++this._access_key.nonce, actions, this.connection.signer, this.accountId, this.connection.networkId); let result; try { @@ -124,7 +131,7 @@ export class Account { } async createAccount(newAccountId: string, publicKey: string, amount: BN): Promise { - const accessKey = createAccessKey(null, "", newAccountId, new BN(0)); + const accessKey = fullAccessKey(); return this.signAndSendTransaction(newAccountId, [createAccount(), transfer(amount), addKey(publicKey, accessKey)]); } @@ -139,8 +146,14 @@ export class Account { return this.signAndSendTransaction(contractId, [functionCall(methodName, Buffer.from(JSON.stringify(args)), gas || DEFAULT_FUNC_CALL_AMOUNT, amount)]); } + // TODO: expand this API to support more options. async addKey(publicKey: string, contractId?: string, methodName?: string, balanceOwner?: string, amount?: BN): Promise { - const accessKey = createAccessKey(contractId ? contractId : null, methodName, balanceOwner, amount); + let accessKey; + if (contractId === null) { + accessKey = fullAccessKey(); + } else { + accessKey = functionCallAccessKey(contractId, methodName === null ? [] : [methodName], amount); + } return this.signAndSendTransaction(this.accountId, [addKey(publicKey, accessKey)]); } diff --git a/src.ts/transaction.ts b/src.ts/transaction.ts index 0da741d182..0e16ceac8b 100644 --- a/src.ts/transaction.ts +++ b/src.ts/transaction.ts @@ -6,7 +6,21 @@ import BN from 'bn.js'; import { base_decode, serialize } from './utils/serialize'; import { Signer } from './signer'; -export class Assignable { +class Enum { + enum: string; + + constructor(properties: any) { + if (Object.keys(properties).length != 1) { + throw new Error("Enum can only take single value"); + } + Object.keys(properties).map((key: string) => { + (this as any)[key] = properties[key]; + this.enum = key; + }); + } +} + +class Assignable { constructor(properties: any) { Object.keys(properties).map((key: any) => { (this as any)[key] = properties[key]; @@ -14,17 +28,30 @@ export class Assignable { } } -export class AccessKey extends Assignable { - contractId: string; methodName: Uint8Array; balanceOwner: string; amount: BN +export class FunctionCallPermission extends Assignable { + allowance?: BN; + receiverId: string; + methodNames: String[]; } -export function createAccessKey(contractId?: string, methodName?: string, balanceOwner?: string, amount?: BN): AccessKey { - return new AccessKey({ - contractId, - methodName, - balanceOwner, - amount: amount || new BN(0), - }); +export class FullAccessPermission extends Assignable {} + +export class AccessKeyPermission extends Enum { + functionCall: FunctionCallPermission; + fullAccess: FullAccessPermission; +} + +export class AccessKey extends Assignable { + nonce: number; + permission: AccessKeyPermission; +} + +export function fullAccessKey(): AccessKey { + return new AccessKey({ nonce: 0, permission: new AccessKeyPermission({fullAccess: new FullAccessPermission({})}) }); +} + +export function functionCallAccessKey(receiverId: string, methodNames: String[], allowance?: BN): AccessKey { + return new AccessKey({ nonce: 0, permission: new AccessKeyPermission({functionCall: new FunctionCallPermission({receiverId, allowance, methodNames})})}); } export class IAction extends Assignable {} @@ -101,8 +128,7 @@ export class SignedTransaction extends Assignable { } } -export class Action { - action: string; +export class Action extends Enum { createAccount: CreateAccount; deployContract: DeployContract; functionCall: FunctionCall; @@ -111,16 +137,6 @@ export class Action { addKey: AddKey; deleteKey: DeleteKey; deleteAccount: DeleteAccount; - - constructor(properties: any) { - if (Object.keys(properties).length != 1) { - throw new Error("Action can only take single value"); - } - Object.keys(properties).map((key: string) => { - (this as any)[key] = properties[key]; - this.action = key; - }); - } } const SCHEMA = { @@ -130,12 +146,20 @@ const SCHEMA = { 'PublicKey': { kind: 'struct', fields: [['keyType', 'u8'], ['data', [32]]] }, 'AccessKey': { kind: 'struct', fields: [ - ['amount', 'u128'], - ['balanceOwner', { kind: 'option', type: 'string' }], - ['contractId', {kind: 'option', type: 'string'}], - ['methodName', {kind: 'option', type: ['u8']}], + ['nonce', 'u64'], + ['permission', AccessKeyPermission], + ]}, + 'AccessKeyPermission': {kind: 'enum', field: 'enum', values: [ + ['functionCall', FunctionCallPermission], + ['fullAccess', FullAccessPermission], + ]}, + 'FunctionCallPermission': {kind: 'struct', fields: [ + ['allowance', {kind: 'option', type: 'u128'}], + ['receiverId', 'string'], + ['methodNames', ['string']], ]}, - 'Action': {kind: 'enum', field: 'action', values: [ + 'FullAccessPermission': {kind: 'struct', fields: []}, + 'Action': {kind: 'enum', field: 'enum', values: [ ['createAccount', CreateAccount], ['deployContract', DeployContract], ['functionCall', functionCall], diff --git a/test/serialize.test.js b/test/serialize.test.js index 10cfcd8901..a4d37c9ffa 100644 --- a/test/serialize.test.js +++ b/test/serialize.test.js @@ -24,18 +24,18 @@ test('serialize object', async () => { test('serialize tx', async() => { const keyStore = new nearlib.keyStores.InMemoryKeyStore(); - await keyStore.setKey("test", "test.near", nearlib.utils.KeyPair.fromString('ed25519:2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw')); - const publicKey = (await keyStore.getKey("test", "test.near")).publicKey; + await keyStore.setKey('test', 'test.near', nearlib.utils.KeyPair.fromString('ed25519:2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw')); + const publicKey = (await keyStore.getKey('test', 'test.near')).publicKey; const actions = [ nearlib.transactions.createAccount(), nearlib.transactions.deployContract(new Uint8Array([1, 2, 3])), - nearlib.transactions.functionCall("qqq", new Uint8Array([1, 2, 3]), 1000, 1000000), + nearlib.transactions.functionCall('qqq', new Uint8Array([1, 2, 3]), 1000, 1000000), nearlib.transactions.transfer(123), nearlib.transactions.stake(1000000, publicKey), - nearlib.transactions.addKey(publicKey, nearlib.transactions.createAccessKey("321", null, "123", 1)), + nearlib.transactions.addKey(publicKey, nearlib.transactions.functionCallAccessKey('zzz', ['www'], null)), nearlib.transactions.deleteKey(publicKey), - nearlib.transactions.deleteAccount("123") + nearlib.transactions.deleteAccount('123') ]; - let [hash, signedTx] = await nearlib.transactions.signTransaction("123", 1, actions, new nearlib.InMemorySigner(keyStore), "test.near", "test"); - expect(nearlib.utils.serialize.base_encode(hash)).toEqual("6jaty3HYh35hQUj2PSN4rhVoZXx5EZ1xUBhBgXjko8fa"); + let [hash, signedTx] = await nearlib.transactions.signTransaction('123', 1, actions, new nearlib.InMemorySigner(keyStore), 'test.near', 'test'); + expect(nearlib.utils.serialize.base_encode(hash)).toEqual('244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM'); }); \ No newline at end of file