Skip to content

Commit

Permalink
feat(aepp,wallet): support delegation signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Jul 25, 2023
1 parent 18bdf5a commit fd0dc43
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 13 deletions.
10 changes: 9 additions & 1 deletion examples/browser/aepp/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
>
Typed data
</a>
<a
href="#"
:class="{ active: view === 'DelegationSignature' }"
@click="view = 'DelegationSignature'"
>
Delegation signature
</a>
</div>

<Component
Expand All @@ -47,10 +54,11 @@ import Basic from './Basic.vue';
import Contracts from './Contracts.vue';
import PayForTx from './PayForTx.vue';
import TypedData from './TypedData.vue';
import DelegationSignature from './DelegationSignature.vue';
export default {
components: {
Connect, Basic, Contracts, PayForTx, TypedData,
Connect, Basic, Contracts, PayForTx, TypedData, DelegationSignature,
},
data: () => ({ view: '' }),
computed: mapState(['aeSdk']),
Expand Down
68 changes: 68 additions & 0 deletions examples/browser/aepp/src/DelegationSignature.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template>
<h2>Sign delegation to contract</h2>
<div class="group">
<div>
<div>Contract address</div>
<div><input v-model="contractAddress"></div>
</div>
<div>
<label>
<input v-model="type" type="radio" value="general">
AENS and oracle
</label>
</div>
<div>
<label>
<input v-model="type" type="radio" value="name">
AENS name
</label>
<div><input v-model="name"></div>
</div>
<div>
<label>
<input v-model="type" type="radio" value="oracle-query">
Oracle query
</label>
<div><input v-model="oracleQueryId"></div>
</div>
<button @click="signPromise = sign()">
Sign
</button>
<div v-if="signPromise">
<div>Signature</div>
<Value :value="signPromise" />
</div>
</div>
</template>

<script>
import { mapState } from 'vuex';
import Value from './components/Value.vue';
export default {
components: { Value },
data: () => ({
type: 'general',
contractAddress: 'ct_6y3N9KqQb74QsvR9NrESyhWeLNiA9aJgJ7ua8CvsTuGot6uzh',
name: 'test.chain',
oracleQueryId: 'oq_6y3N9KqQb74QsvR9NrESyhWeLNiA9aJgJ7ua8CvsTuGot6uzh',
signPromise: null,
}),
computed: mapState(['aeSdk']),
methods: {
sign() {
switch (this.type) {
case 'general':
return this.aeSdk.signDelegationToContract(this.contractAddress);
case 'name':
return this.aeSdk.signNameDelegationToContract(this.contractAddress, this.name);
case 'oracle-query':
return this.aeSdk
.signOracleQueryDelegationToContract(this.contractAddress, this.oracleQueryId);
default:
throw new Error(`Unknown delegation signature type: ${this.type}`)
}
},
},
};
</script>
40 changes: 40 additions & 0 deletions examples/browser/wallet-iframe/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,46 @@ export default {
return super.signTypedData(data, aci, options);
}
async signDelegationToContract(
contractAddress,
networkId,
{ aeppRpcClientId: id, aeppOrigin, ...options },
) {
if (id != null) {
const opt = { ...options, contractAddress, networkId };
genConfirmCallback('sign delegation to contract')(id, opt, aeppOrigin);
}
return super.signDelegationToContract(contractAddress, networkId, options);
}
async signNameDelegationToContract(
contractAddress,
name,
networkId,
{ aeppRpcClientId: id, aeppOrigin, ...options },
) {
if (id != null) {
const opt = { ...options, contractAddress, name, networkId };
genConfirmCallback('sign delegation of name to contract')(id, opt, aeppOrigin);
}
return super.signNameDelegationToContract(contractAddress, name, networkId, options);
}
async signOracleQueryDelegationToContract(
contractAddress,
oracleQueryId,
networkId,
{ aeppRpcClientId: id, aeppOrigin, ...options },
) {
if (id != null) {
const opt = { ...options, contractAddress, oracleQueryId, networkId };
genConfirmCallback('sign delegation of oracle query to contract')(id, opt, aeppOrigin);
}
return super.signOracleQueryDelegationToContract(
contractAddress, oracleQueryId, networkId, options,
);
}
static generate() {
// TODO: can inherit parent method after implementing https://github.com/aeternity/aepp-sdk-js/issues/1672
return new AccountMemoryProtected(generateKeyPair().secretKey);
Expand Down
40 changes: 40 additions & 0 deletions examples/browser/wallet-web-extension/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,46 @@ class AccountMemoryProtected extends MemoryAccount {
return super.signTypedData(data, aci, options);
}

async signDelegationToContract(
contractAddress,
networkId,
{ aeppRpcClientId: id, aeppOrigin, ...options },
) {
if (id != null) {
const opt = { ...options, contractAddress, networkId };
await genConfirmCallback('sign delegation to contract')(id, opt, aeppOrigin);
}
return super.signDelegationToContract(contractAddress, networkId, options);
}

async signNameDelegationToContract(
contractAddress,
name,
networkId,
{ aeppRpcClientId: id, aeppOrigin, ...options },
) {
if (id != null) {
const opt = { ...options, contractAddress, name, networkId };
await genConfirmCallback('sign delegation of name to contract')(id, opt, aeppOrigin);
}
return super.signNameDelegationToContract(contractAddress, name, networkId, options);
}

async signOracleQueryDelegationToContract(
contractAddress,
oracleQueryId,
networkId,
{ aeppRpcClientId: id, aeppOrigin, ...options },
) {
if (id != null) {
const opt = { ...options, contractAddress, oracleQueryId, networkId };
await genConfirmCallback('sign delegation of oracle query to contract')(id, opt, aeppOrigin);
}
return super.signOracleQueryDelegationToContract(
contractAddress, oracleQueryId, networkId, options,
);
}

static generate() {
// TODO: can inherit parent method after implementing https://github.com/aeternity/aepp-sdk-js/issues/1672
return new AccountMemoryProtected(generateKeyPair().secretKey);
Expand Down
18 changes: 18 additions & 0 deletions src/AeSdkWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,24 @@ export default class AeSdkWallet extends AeSdk {
signature: await this.signTypedData(data, aci, parameters),
};
},
[METHODS.signDelegationToContract]: async ({
contractAddress, name, oracleQueryId, onAccount = this.address,
}, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
if (!this.addresses().includes(onAccount)) {
throw new RpcPermissionDenyError(onAccount);
}

const parameters = { onAccount, aeppOrigin: origin, aeppRpcClientId: id };
const signature = await (
(name == null ? null : this
.signNameDelegationToContract(contractAddress, name, parameters))
?? (oracleQueryId == null ? null : this
.signOracleQueryDelegationToContract(contractAddress, oracleQueryId, parameters))
?? this.signDelegationToContract(contractAddress, parameters)
);
return { signature };
},
},
),
};
Expand Down
38 changes: 29 additions & 9 deletions src/account/Rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ArgumentError, NotImplementedError, UnsupportedProtocolError } from '..
import { Encoded } from '../utils/encoder';
import RpcClient from '../aepp-wallet-communication/rpc/RpcClient';
import { AeppApi, WalletApi } from '../aepp-wallet-communication/rpc/types';
import { AensName } from '../tx/builder/constants';

/**
* Account provided by wallet
Expand Down Expand Up @@ -70,18 +71,37 @@ 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');
override async signDelegationToContract(
contractAddress: Encoded.ContractAddress,
): Promise<Encoded.Signature> {
const { signature } = await this._rpcClient.request(METHODS.signDelegationToContract, {
onAccount: this.address,
contractAddress,
});
return signature;
}

// eslint-disable-next-line class-methods-use-this
override async signNameDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using wallet');
override async signNameDelegationToContract(
contractAddress: Encoded.ContractAddress,
name: AensName,
): Promise<Encoded.Signature> {
const { signature } = await this._rpcClient.request(METHODS.signDelegationToContract, {
onAccount: this.address,
contractAddress,
name,
});
return signature;
}

// eslint-disable-next-line class-methods-use-this
override async signOracleQueryDelegationToContract(): Promise<Encoded.Signature> {
throw new NotImplementedError('signing delegation to contract using wallet');
override async signOracleQueryDelegationToContract(
contractAddress: Encoded.ContractAddress,
oracleQueryId: Encoded.OracleQueryId,
): Promise<Encoded.Signature> {
const { signature } = await this._rpcClient.request(METHODS.signDelegationToContract, {
onAccount: this.address,
contractAddress,
oracleQueryId,
});
return signature;
}
}
10 changes: 10 additions & 0 deletions src/aepp-wallet-communication/rpc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Domain, AciValue } from '../../utils/typed-data';
import { METHODS, SUBSCRIPTION_TYPES, WALLET_TYPE } from '../schema';
import { TransformNodeType } from '../../Node';
import { SignedTx } from '../../apis/node';
import { AensName } from '../../tx/builder/constants';

export interface WalletInfo {
id: string;
Expand Down Expand Up @@ -82,6 +83,15 @@ export interface WalletApi {
onAccount: Encoded.AccountAddress;
},
) => Promise<{ signature: Encoded.Signature }>;

[METHODS.signDelegationToContract]: (
p: {
contractAddress: Encoded.ContractAddress;
name?: AensName;
oracleQueryId?: Encoded.OracleQueryId;
onAccount: Encoded.AccountAddress;
},
) => Promise<{ signature: Encoded.Signature }>;
}

export interface AeppApi {
Expand Down
1 change: 1 addition & 0 deletions src/aepp-wallet-communication/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const enum METHODS {
sign = 'transaction.sign',
signMessage = 'message.sign',
signTypedData = 'typedData.sign',
signDelegationToContract = 'delegationToContract.sign',
subscribeAddress = 'address.subscribe',
updateNetwork = 'networkId.update',
closeConnection = 'connection.close',
Expand Down
42 changes: 39 additions & 3 deletions test/integration/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ describe('Aepp<->Wallet', function aeppWallet() {
recipientId: keypair.publicKey,
amount: 0,
});
await expect(aepp.signTransaction(tx, { onAccount: account.address }))
await expect(aepp.signTransaction(tx))
.to.be.rejectedWith('The peer failed to execute your request due to unknown error');
});

Expand Down Expand Up @@ -380,7 +380,7 @@ describe('Aepp<->Wallet', function aeppWallet() {
wallet._resolveAccount().signMessage = () => {
throw new Error('test');
};
await expect(aepp.signMessage('test', { onAccount: account.address }))
await expect(aepp.signMessage('test'))
.to.be.rejectedWith('The peer failed to execute your request due to unknown error');
});

Expand Down Expand Up @@ -424,7 +424,7 @@ describe('Aepp<->Wallet', function aeppWallet() {
wallet._resolveAccount().signTypedData = () => {
throw new Error('test');
};
await expect(aepp.signTypedData(recordData, recordAci, { onAccount: account.address }))
await expect(aepp.signTypedData(recordData, recordAci))
.to.be.rejectedWith('The peer failed to execute your request due to unknown error');
});

Expand All @@ -436,6 +436,42 @@ describe('Aepp<->Wallet', function aeppWallet() {
});
});

describe('Sign delegation to contract', () => {
const contractAddress = 'ct_6y3N9KqQb74QsvR9NrESyhWeLNiA9aJgJ7ua8CvsTuGot6uzh';

it('rejected by wallet', async () => {
let origin;
wallet._resolveAccount().signDelegationToContract = (ct, netId, { aeppOrigin } = {}) => {
origin = aeppOrigin;
throw new RpcRejectedByUserError();
};
await expect(aepp.signDelegationToContract(contractAddress)).to.be.eventually
.rejectedWith('Operation rejected by user').with.property('code', 4);
expect(origin).to.be.equal('http://origin.test');
});

it('works', async () => {
// @ts-expect-error removes object property to restore the original behavior
delete wallet._resolveAccount().signDelegationToContract;
const signature = await aepp.signDelegationToContract(contractAddress);
expect(signature).to.satisfy((s: string) => s.startsWith('sg_'));
});

it('fails with unknown error', async () => {
wallet._resolveAccount().signDelegationToContract = () => {
throw new Error('test');
};
await expect(aepp.signDelegationToContract(contractAddress))
.to.be.rejectedWith('The peer failed to execute your request due to unknown error');
});

it('signs using specific account', async () => {
const onAccount = wallet.addresses()[1];
const signature = await aepp.signDelegationToContract(contractAddress, { onAccount });
expect(signature).to.satisfy((s: string) => s.startsWith('sg_'));
});
});

it('Sign and broadcast invalid transaction', async () => {
// @ts-expect-error removes object property to restore the original behavior
delete wallet._resolveAccount().signTransaction;
Expand Down

0 comments on commit fd0dc43

Please sign in to comment.