From 18bdf5a113afac2c9a0c99555e2e301f043fa3ab Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Mon, 24 Jul 2023 20:44:59 +0600 Subject: [PATCH] feat(account): add methods to generate delegation signatures --- src/AeSdkBase.ts | 40 +++++++++++++++ src/account/Base.ts | 76 +++++++++++++++++++++++++++- src/account/Generalized.ts | 15 ++++++ src/account/Ledger.ts | 15 ++++++ src/account/Memory.ts | 51 ++++++++++++++++++- src/account/Rpc.ts | 15 ++++++ src/contract/delegation-signature.ts | 48 ++++++++++++++---- test/integration/contract.ts | 7 +++ 8 files changed, 254 insertions(+), 13 deletions(-) diff --git a/src/AeSdkBase.ts b/src/AeSdkBase.ts index 709639ad79..21fc3f5da9 100644 --- a/src/AeSdkBase.ts +++ b/src/AeSdkBase.ts @@ -1,11 +1,13 @@ import Node from './Node'; import AccountBase from './account/Base'; import { + ArgumentError, CompilerError, DuplicateNodeError, NodeNotFoundError, NotImplementedError, TypeError, } from './utils/errors'; import { Encoded } from './utils/encoder'; import CompilerBase from './contract/compiler/Base'; import AeSdkMethods, { OnAccount, getValueOrErrorProxy, AeSdkMethodsOptions } from './AeSdkMethods'; +import { AensName } from './tx/builder/constants'; type NodeInfo = Awaited> & { name: string }; @@ -176,6 +178,44 @@ export default class AeSdkBase extends AeSdkMethods { return this._resolveAccount(onAccount).signTypedData(data, aci, options); } + async signDelegationToContract( + contractAddress: Encoded.ContractAddress, + { onAccount, ...options }: { onAccount?: OnAccount; networkId?: string } + & Parameters[2] = {}, + ): Promise { + const networkId = options?.networkId + ?? (this.selectedNodeName !== null ? await this.api.getNetworkId() : undefined); + if (networkId == null) throw new ArgumentError('networkId', 'provided', networkId); + return this._resolveAccount(onAccount) + .signDelegationToContract(contractAddress, networkId, options); + } + + async signNameDelegationToContract( + contractAddress: Encoded.ContractAddress, + name: AensName, + { onAccount, ...options }: { onAccount?: OnAccount; networkId?: string } + & Parameters[3] = {}, + ): Promise { + const networkId = options?.networkId + ?? (this.selectedNodeName !== null ? await this.api.getNetworkId() : undefined); + if (networkId == null) throw new ArgumentError('networkId', 'provided', networkId); + return this._resolveAccount(onAccount) + .signNameDelegationToContract(contractAddress, name, networkId, options); + } + + async signOracleQueryDelegationToContract( + contractAddress: Encoded.ContractAddress, + oracleQueryId: Encoded.OracleQueryId, + { onAccount, ...options }: { onAccount?: OnAccount; networkId?: string } + & Parameters[3] = {}, + ): Promise { + const networkId = options?.networkId + ?? (this.selectedNodeName !== null ? await this.api.getNetworkId() : undefined); + if (networkId == null) throw new ArgumentError('networkId', 'provided', networkId); + return this._resolveAccount(onAccount) + .signOracleQueryDelegationToContract(contractAddress, oracleQueryId, networkId, options); + } + override _getOptions(callOptions: AeSdkMethodsOptions = {}): { onNode: Node; onAccount: AccountBase; diff --git a/src/account/Base.ts b/src/account/Base.ts index 852624371a..9bd11a2529 100644 --- a/src/account/Base.ts +++ b/src/account/Base.ts @@ -1,7 +1,7 @@ import { Encoded } from '../utils/encoder'; import Node from '../Node'; import CompilerBase from '../contract/compiler/Base'; -import { Int } from '../tx/builder/constants'; +import { AensName, Int } from '../tx/builder/constants'; import { AciValue, Domain } from '../utils/typed-data'; import { NotImplementedError } from '../utils/errors'; @@ -76,6 +76,80 @@ export default abstract class AccountBase { throw new NotImplementedError('signTypedData method'); } + /** + * Sign delegation of AENS, oracle operations to a contract + * @param contractAddress - Address of a contract to delegate permissions to + * @param options - Options + * @returns Signature + */ + // TODO: make abstract in the next major release + // eslint-disable-next-line class-methods-use-this + async signDelegationToContract( + /* eslint-disable @typescript-eslint/no-unused-vars */ + contractAddress: Encoded.ContractAddress, + networkId: string, + options?: { + aeppOrigin?: string; + aeppRpcClientId?: string; + }, + /* eslint-enable @typescript-eslint/no-unused-vars */ + ): Promise { + throw new NotImplementedError('signDelegationToContract method'); + } + + /** + * Sign delegation of an AENS name to a contract + * @param contractAddress - Address of a contract to delegate permissions to + * @param name - AENS name to manage by a contract + * @param options - Options + * @returns Signature + */ + // TODO: make abstract in the next major release + // eslint-disable-next-line class-methods-use-this + async signNameDelegationToContract( + /* eslint-disable @typescript-eslint/no-unused-vars */ + contractAddress: Encoded.ContractAddress, + name: AensName, + networkId: string, + options?: { + aeppOrigin?: string; + aeppRpcClientId?: string; + }, + /* eslint-enable @typescript-eslint/no-unused-vars */ + ): Promise { + throw new NotImplementedError('signNameDelegationToContract method'); + } + + /** + * Sign delegation of oracle query to a contract + * + * Warning! Implementations needs to ensure that decoded oracle query id is not equal to decoded + * current account address unless https://github.com/aeternity/aesophia/issues/475 is fixed. + * + * Warning! Implementations needs to ensure that oracle query and contract exists unless + * https://github.com/aeternity/aesophia/issues/474 is fixed. + * + * @param contractAddress - Address of a contract to delegate permissions to + * @param oracleQueryId - Oracle query ID to reply by a contract + * @param options - Options + * @returns Signature + */ + // TODO: make abstract in the next major release + // eslint-disable-next-line class-methods-use-this + async signOracleQueryDelegationToContract( + /* eslint-disable @typescript-eslint/no-unused-vars */ + contractAddress: Encoded.ContractAddress, + oracleQueryId: Encoded.OracleQueryId, + networkId: string, + options?: { + aeppOrigin?: string; + aeppRpcClientId?: string; + }, + /* eslint-enable @typescript-eslint/no-unused-vars */ + ): Promise { + throw new NotImplementedError('signOracleQueryDelegationToContract method'); + } + /** * Sign data blob * @param data - Data blob to sign diff --git a/src/account/Generalized.ts b/src/account/Generalized.ts index 002dfc8c0e..a0196acf6f 100644 --- a/src/account/Generalized.ts +++ b/src/account/Generalized.ts @@ -43,6 +43,21 @@ export default class AccountGeneralized extends AccountBase { throw new NotImplementedError('Can\'t sign using generalized account'); } + // eslint-disable-next-line class-methods-use-this + override async signDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using generalized account'); + } + + // eslint-disable-next-line class-methods-use-this + override async signNameDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using generalized account'); + } + + // eslint-disable-next-line class-methods-use-this + override async signOracleQueryDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using generalized account'); + } + override async signTransaction( tx: Encoded.Transaction, { authData, onCompiler, onNode }: Parameters[1], diff --git a/src/account/Ledger.ts b/src/account/Ledger.ts index ff95eb1591..1f4e3e970b 100644 --- a/src/account/Ledger.ts +++ b/src/account/Ledger.ts @@ -45,6 +45,21 @@ export default class AccountLedger extends AccountBase { throw new NotImplementedError('Typed data signing using Ledger HW'); } + // eslint-disable-next-line class-methods-use-this + override async signDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using Ledger HW'); + } + + // eslint-disable-next-line class-methods-use-this + override async signNameDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using Ledger HW'); + } + + // eslint-disable-next-line class-methods-use-this + override async signOracleQueryDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using Ledger HW'); + } + override async signTransaction( tx: Encoded.Transaction, { innerTx, networkId }: { innerTx?: boolean; networkId?: string } = {}, diff --git a/src/account/Memory.ts b/src/account/Memory.ts index ec1a3da388..5620abb670 100644 --- a/src/account/Memory.ts +++ b/src/account/Memory.ts @@ -9,7 +9,8 @@ import { import { concatBuffers } from '../utils/other'; import { hashTypedData, AciValue } from '../utils/typed-data'; import { buildTx } from '../tx/builder'; -import { Tag } from '../tx/builder/constants'; +import { Tag, AensName } from '../tx/builder/constants'; +import { produceNameId } from '../tx/builder/helpers'; const secretKeys = new WeakMap(); @@ -89,4 +90,52 @@ export default class AccountMemory extends AccountBase { const signature = await this.sign(dHash, options); return encode(signature, Encoding.Signature); } + + override async signDelegationToContract( + contractAddress: Encoded.ContractAddress, + networkId: string, + ): Promise { + const payload = concatBuffers([ + Buffer.from(networkId), + decode(this.address), + decode(contractAddress), + ]); + const signature = await this.sign(payload); + return encode(signature, Encoding.Signature); + } + + override async signNameDelegationToContract( + contractAddress: Encoded.ContractAddress, + name: AensName, + networkId: string, + ): Promise { + const payload = concatBuffers([ + Buffer.from(networkId), + decode(this.address), + decode(produceNameId(name)), + decode(contractAddress), + ]); + const signature = await this.sign(payload); + return encode(signature, Encoding.Signature); + } + + override async signOracleQueryDelegationToContract( + contractAddress: Encoded.ContractAddress, + oracleQueryId: Encoded.OracleQueryId, + networkId: string, + ): Promise { + const oracleQueryIdDecoded = decode(oracleQueryId); + const addressDecoded = decode(this.address); + // TODO: remove after fixing https://github.com/aeternity/aesophia/issues/475 + if (oracleQueryIdDecoded.compare(addressDecoded) === 0) { + throw new ArgumentError('oracleQueryId', 'not equal to account address', oracleQueryId); + } + const payload = concatBuffers([ + Buffer.from(networkId), + oracleQueryIdDecoded, + decode(contractAddress), + ]); + const signature = await this.sign(payload); + return encode(signature, Encoding.Signature); + } } diff --git a/src/account/Rpc.ts b/src/account/Rpc.ts index 7c7984e436..04562289bd 100644 --- a/src/account/Rpc.ts +++ b/src/account/Rpc.ts @@ -69,4 +69,19 @@ export default class AccountRpc extends AccountBase { }); return signature; } + + // eslint-disable-next-line class-methods-use-this + override async signDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using wallet'); + } + + // eslint-disable-next-line class-methods-use-this + override async signNameDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using wallet'); + } + + // eslint-disable-next-line class-methods-use-this + override async signOracleQueryDelegationToContract(): Promise { + throw new NotImplementedError('signing delegation to contract using wallet'); + } } diff --git a/src/contract/delegation-signature.ts b/src/contract/delegation-signature.ts index b83bc2b7a5..6a8fa9643a 100644 --- a/src/contract/delegation-signature.ts +++ b/src/contract/delegation-signature.ts @@ -1,10 +1,14 @@ import { decode, Encoded } from '../utils/encoder'; +import { ArgumentError } from '../utils/errors'; import { AensName } from '../tx/builder/constants'; import AccountBase from '../account/Base'; -import { concatBuffers } from '../utils/other'; -import { isNameValid, produceNameId } from '../tx/builder/helpers'; +import { isNameValid } from '../tx/builder/helpers'; import Node from '../Node'; +function ensureOracleQuery(oq: string): asserts oq is Encoded.OracleQueryId { + if (!oq.startsWith('oq_')) throw new ArgumentError('oq', 'oracle query', oq); +} + /** * Helper to generate a signature to delegate * - pre-claim/claim/transfer/revoke of a name to a contract. @@ -17,6 +21,7 @@ import Node from '../Node'; * @param options.onAccount - Account to use * @param options.onNode - Node to use * @returns Signature + * @deprecated use methods `sign*DelegationToContract` of Account instance instead * @example * ```js * const aeSdk = new AeSdk({ ... }) @@ -42,15 +47,36 @@ import Node from '../Node'; export default async function createDelegationSignature( contractAddress: Encoded.ContractAddress, ids: Array, - options: { omitAddress?: boolean; onAccount: AccountBase; onNode: Node }, + { onAccount, omitAddress, ...options }: { + omitAddress?: boolean; + onAccount: AccountBase; + onNode: Node; + }, ): Promise { - return options.onAccount.sign( - concatBuffers([ - Buffer.from(await options.onNode.getNetworkId()), - ...options.omitAddress === true ? [] : [decode(options.onAccount.address)], - ...ids.map((e) => (isNameValid(e) ? produceNameId(e) : e)).map((e) => decode(e)), - decode(contractAddress), - ]), - options, + if (ids.length > 1) throw new ArgumentError('ids', 'shorter than 2', ids); + const networkId = await options.onNode.getNetworkId(); + if (ids.length === 0) { + if (omitAddress === true) { + throw new ArgumentError('omitAddress', 'equal false', omitAddress); + } + return decode(await onAccount.signDelegationToContract(contractAddress, networkId)); + } + + const [payload] = ids; + if (isNameValid(payload)) { + if (omitAddress === true) { + throw new ArgumentError('omitAddress', 'equal false', omitAddress); + } + return decode( + await onAccount.signNameDelegationToContract(contractAddress, payload, networkId), + ); + } + + ensureOracleQuery(payload); + if (omitAddress !== true) { + throw new ArgumentError('omitAddress', 'equal true', omitAddress); + } + return decode( + await onAccount.signOracleQueryDelegationToContract(contractAddress, payload, networkId), ); } diff --git a/test/integration/contract.ts b/test/integration/contract.ts index 669b75a308..35064ece28 100644 --- a/test/integration/contract.ts +++ b/test/integration/contract.ts @@ -504,5 +504,12 @@ describe('Contract', () => { const queryObject2 = await aeSdk.getQueryObject(oracle.id, queryId); queryObject2.decodedResponse.should.be.equal(r); }); + + it('fails trying to create general delegation as oracle query', async () => { + const fakeQueryId = encode(decode(aeSdk.address), Encoding.OracleQueryId); + await expect( + aeSdk.createDelegationSignature(contractAddress, [fakeQueryId], { omitAddress: true }), + ).to.be.rejectedWith('not equal to account address'); + }); }); });