Skip to content

Commit

Permalink
feat(account): add methods to generate delegation signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Jul 25, 2023
1 parent fc6e42f commit 18bdf5a
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 13 deletions.
40 changes: 40 additions & 0 deletions src/AeSdkBase.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<Node['getNodeInfo']>> & { name: string };

Expand Down Expand Up @@ -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<AccountBase['signDelegationToContract']>[2] = {},
): Promise<Encoded.Signature> {
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<AccountBase['signNameDelegationToContract']>[3] = {},
): Promise<Encoded.Signature> {
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<AccountBase['signOracleQueryDelegationToContract']>[3] = {},
): Promise<Encoded.Signature> {
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;
Expand Down
76 changes: 75 additions & 1 deletion src/account/Base.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<Encoded.Signature> {
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<Encoded.Signature> {
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<Encoded.Signature> {
throw new NotImplementedError('signOracleQueryDelegationToContract method');
}

/**
* Sign data blob
* @param data - Data blob to sign
Expand Down
15 changes: 15 additions & 0 deletions src/account/Generalized.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using generalized account');
}

// eslint-disable-next-line class-methods-use-this
override async signNameDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using generalized account');
}

// eslint-disable-next-line class-methods-use-this
override async signOracleQueryDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using generalized account');
}

override async signTransaction(
tx: Encoded.Transaction,
{ authData, onCompiler, onNode }: Parameters<AccountBase['signTransaction']>[1],
Expand Down
15 changes: 15 additions & 0 deletions src/account/Ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using Ledger HW');
}

// eslint-disable-next-line class-methods-use-this
override async signNameDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using Ledger HW');
}

// eslint-disable-next-line class-methods-use-this
override async signOracleQueryDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using Ledger HW');
}

override async signTransaction(
tx: Encoded.Transaction,
{ innerTx, networkId }: { innerTx?: boolean; networkId?: string } = {},
Expand Down
51 changes: 50 additions & 1 deletion src/account/Memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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<Encoded.Signature> {
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<Encoded.Signature> {
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<Encoded.Signature> {
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);
}
}
15 changes: 15 additions & 0 deletions src/account/Rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,19 @@ export default class AccountRpc extends AccountBase {
});
return signature;
}

// eslint-disable-next-line class-methods-use-this
override async signDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using wallet');
}

// eslint-disable-next-line class-methods-use-this
override async signNameDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using wallet');
}

// eslint-disable-next-line class-methods-use-this
override async signOracleQueryDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using wallet');
}
}
48 changes: 37 additions & 11 deletions src/contract/delegation-signature.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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({ ... })
Expand All @@ -42,15 +47,36 @@ import Node from '../Node';
export default async function createDelegationSignature(
contractAddress: Encoded.ContractAddress,
ids: Array<Encoded.Any | AensName>,
options: { omitAddress?: boolean; onAccount: AccountBase; onNode: Node },
{ onAccount, omitAddress, ...options }: {
omitAddress?: boolean;
onAccount: AccountBase;
onNode: Node;
},
): Promise<Uint8Array> {
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),
);
}
7 changes: 7 additions & 0 deletions test/integration/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});

0 comments on commit 18bdf5a

Please sign in to comment.