Skip to content

Commit

Permalink
feat(aepp,wallet): support inner transaction signing
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed May 27, 2023
1 parent 0dcb239 commit 725782b
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 24 deletions.
12 changes: 11 additions & 1 deletion examples/browser/aepp/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
>
Smart contracts
</a>
<a
href="#"
:class="{ active: view === 'PayForTx' }"
@click="view = 'PayForTx'"
>
Pay for transaction
</a>
</div>

<Component
Expand All @@ -31,9 +38,12 @@ import { mapState } from 'vuex';
import Connect from './Connect.vue';
import Basic from './Basic.vue';
import Contracts from './Contracts.vue';
import PayForTx from './PayForTx.vue';
export default {
components: { Connect, Basic, Contracts },
components: {
Connect, Basic, Contracts, PayForTx,
},
data: () => ({ view: '' }),
computed: mapState(['aeSdk']),
};
Expand Down
72 changes: 72 additions & 0 deletions examples/browser/aepp/src/PayForTx.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<GenerateSpendTx />

<h2>Sign inner transaction</h2>
<div class="group">
<div>
<div>Transaction</div>
<div>
<input
v-model="txToPayFor"
placeholder="tx_..."
>
</div>
</div>
<button @click="signInnerTxPromise = signInnerTx()">
Sign
</button>
<div v-if="signInnerTxPromise">
<div>Signed inner transaction</div>
<Value :value="signInnerTxPromise" />
</div>
</div>

<h2>Pay for transaction</h2>
<div class="group">
<div>
<div>Signed inner transaction</div>
<div>
<input
v-model="innerTx"
placeholder="tx_..."
>
</div>
</div>
<button @click="payForTxPromise = payForTx()">
Pay for transaction
</button>
<div v-if="payForTxPromise">
<div>Result</div>
<Value :value="payForTxPromise" />
</div>
</div>
</template>

<script>
import { mapState } from 'vuex';
import Value from './components/Value.vue';
import SpendCoins from './components/SpendCoins.vue';
import MessageSign from './components/MessageSign.vue';
import GenerateSpendTx from './components/GenerateSpendTx.vue';
export default {
components: {
Value, SpendCoins, MessageSign, GenerateSpendTx,
},
data: () => ({
innerTx: '',
txToPayFor: '',
signInnerTxPromise: null,
payForTxPromise: null,
}),
computed: mapState(['aeSdk']),
methods: {
signInnerTx() {
return this.aeSdk.signTransaction(this.txToPayFor, { innerTx: true });
},
payForTx() {
return this.aeSdk.payForTransaction(this.innerTx);
},
},
};
</script>
76 changes: 76 additions & 0 deletions examples/browser/aepp/src/components/GenerateSpendTx.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<h2>Generate spend transaction</h2>
<div class="group">
<div>
<div>Recipient address</div>
<div>
<input
v-model="spendTo"
placeholder="ak_..."
>
</div>
</div>
<div>
<div>Coins amount</div>
<div><input v-model="spendAmount"></div>
</div>
<div>
<div>Payload</div>
<div><input v-model="spendPayload"></div>
</div>
<div>
<div>Increment nonce by 1</div>
<div>
<input
type="checkbox"
v-model="incrementNonce"
>
(only if you want to pay for this transaction yourself)
</div>
</div>
<button @click="generatePromise = generate()">
Generate
</button>
<div v-if="generatePromise">
<div>Spend transaction</div>
<Value :value="generatePromise" />
</div>
</div>
</template>

<script>
import { mapState } from 'vuex';
import {
encode, Encoding, Tag, unpackTx, buildTx,
} from '@aeternity/aepp-sdk';
import Value from './Value.vue';
export default {
components: { Value },
data: () => ({
spendTo: '',
spendAmount: '',
spendPayload: '',
incrementNonce: true,
generatePromise: null,
}),
computed: mapState(['aeSdk']),
methods: {
async generate() {
let spendTx = await this.aeSdk.buildTx({
tag: Tag.SpendTx,
senderId: this.aeSdk.address,
recipientId: this.spendTo,
amount: this.spendAmount,
payload: encode(new TextEncoder().encode(this.spendPayload), Encoding.Bytearray),
});
if (this.incrementNonce) {
const spendTxParams = unpackTx(spendTx);
spendTxParams.nonce += 1;
spendTx = buildTx(spendTxParams);
}
return spendTx;
},
},
};
</script>
2 changes: 1 addition & 1 deletion examples/browser/aepp/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ h2 {
@extend .mt-2, .font-bold, .text-2xl;
}

input:not([type=radio]), textarea {
input:not([type=radio]):not([type=checkbox]), textarea {
@extend .bg-gray-900, .text-white, .p-2, .w-full;
}

Expand Down
13 changes: 10 additions & 3 deletions src/AeSdkWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,21 @@ export default class AeSdkWallet extends AeSdk {
await this.onAskAccounts(id, params, origin);
return this.addresses();
},
[METHODS.sign]: async ({ tx, onAccount = this.address, returnSigned }, origin) => {
[METHODS.sign]: async (
{
tx, onAccount = this.address, returnSigned, innerTx,
},
origin,
) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
if (!this.addresses().includes(onAccount)) {
throw new RpcPermissionDenyError(onAccount);
}

const parameters = { onAccount, aeppOrigin: origin, aeppRpcClientId: id };
if (returnSigned) {
const parameters = {
onAccount, aeppOrigin: origin, aeppRpcClientId: id, innerTx,
};
if (returnSigned || innerTx === true) {
return { signedTransaction: await this.signTransaction(tx, parameters) };
}
try {
Expand Down
2 changes: 1 addition & 1 deletion src/account/Rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ export default class AccountRpc extends AccountBase {
tx: Encoded.Transaction,
{ innerTx, networkId }: Parameters<AccountBase['signTransaction']>[1] = {},
): Promise<Encoded.Transaction> {
if (innerTx != null) throw new NotImplementedError('innerTx option in AccountRpc');
if (networkId == null) throw new ArgumentError('networkId', 'provided', networkId);
const res = await this._rpcClient.request(METHODS.sign, {
onAccount: this.address,
tx,
returnSigned: true,
networkId,
innerTx,
});
if (res.signedTransaction == null) {
throw new UnsupportedProtocolError('signedTransaction is missed in wallet response');
Expand Down
1 change: 1 addition & 0 deletions src/aepp-wallet-communication/rpc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface WalletApi {
* @see {@link https://github.com/aeternity/aepp-sdk-js/commit/153fd89a52c4eab39fcd659b356b36d32129c1ba}
*/
networkId: string;
innerTx?: boolean;
}
) => Promise<{
/**
Expand Down
1 change: 1 addition & 0 deletions src/tx/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ validators.push(
return [{ message, key: 'InvalidAccountType', checkedKeys: ['tag'] }];
},
// TODO: revert nonce check
// TODO: ensure nonce valid when paying for own tx
(tx, { consensusProtocolVersion }) => {
const oracleCall = Tag.Oracle === tx.tag || Tag.OracleRegisterTx === tx.tag;
const contractCreate = Tag.ContractCreateTx === tx.tag || Tag.GaAttachTx === tx.tag;
Expand Down
35 changes: 17 additions & 18 deletions test/integration/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
METHODS,
RPC_STATUS,
generateKeyPair,
hash,
verify,
NoWalletConnectedError,
UnAuthorizedAccountError,
Expand All @@ -32,7 +31,7 @@ import {
verifyMessage,
buildTx,
} from '../../src';
import { concatBuffers } from '../../src/utils/other';
import { getBufferToSign } from '../../src/account/Memory';
import { ImplPostMessage } from '../../src/aepp-wallet-communication/connection/BrowserWindowMessage';
import {
getSdk, ignoreVersion, networkId, url, compilerUrl,
Expand Down Expand Up @@ -300,23 +299,23 @@ describe('Aepp<->Wallet', function aeppWallet() {
.to.be.rejectedWith('The peer failed to execute your request due to unknown error');
});

it('Sign transaction: wallet allow', async () => {
// @ts-expect-error removes object property to restore the original behavior
delete wallet._resolveAccount().signTransaction;
const tx = await aepp.buildTx({
tag: Tag.SpendTx,
senderId: aepp.address,
recipientId: aepp.address,
amount: 0,
});
[false, true].forEach((innerTx) => {
it(`Sign${innerTx ? ' inner' : ''} transaction`, async () => {
// @ts-expect-error removes object property to restore the original behavior
delete wallet._resolveAccount().signTransaction;
const tx = await aepp.buildTx({
tag: Tag.SpendTx,
senderId: aepp.address,
recipientId: aepp.address,
amount: 0,
});

const signedTx = await aepp.signTransaction(tx);
const unpackedTx = unpackTx(signedTx, Tag.SignedTx);
const { signatures: [signature], encodedTx } = unpackedTx;
const txWithNetwork = concatBuffers([
Buffer.from(networkId), hash(decode(buildTx(encodedTx))),
]);
expect(verify(txWithNetwork, signature, aepp.address)).to.be.equal(true);
const signedTx = await aepp.signTransaction(tx, { innerTx });
const unpackedTx = unpackTx(signedTx, Tag.SignedTx);
const { signatures: [signature], encodedTx } = unpackedTx;
const txWithNetwork = getBufferToSign(buildTx(encodedTx), networkId, innerTx);
expect(verify(txWithNetwork, signature, aepp.address)).to.be.equal(true);
});
});

it('Try to sign using unpermited account', async () => {
Expand Down

0 comments on commit 725782b

Please sign in to comment.