Skip to content

Commit

Permalink
feat(aepp)!: add api to ask wallet to select network
Browse files Browse the repository at this point in the history
BREAKING CHANGE: AeSdkWallet requires `onAskToSelectNetwork` constructor option
Provide a function throwing `RpcMethodNotFoundError` if you don't want to support network change by
aepp.
  • Loading branch information
davidyuk committed Sep 26, 2024
1 parent 2eaf479 commit 9871c91
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 19 deletions.
4 changes: 4 additions & 0 deletions examples/browser/aepp/src/Connect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
</template>
</div>

<SelectNetwork :select="(network) => this.walletConnector.askToSelectNetwork(network)" />

<h2>Ledger Hardware Wallet</h2>
<div class="group">
<template v-if="ledgerStatus">
Expand Down Expand Up @@ -123,8 +125,10 @@ import {
} from '@aeternity/aepp-sdk';
import { mapState } from 'vuex';
import TransportWebUSB from '@ledgerhq/hw-transport-webusb';
import SelectNetwork from './components/SelectNetwork.vue';
export default {
components: { SelectNetwork },
data: () => ({
connectMethod: 'default',
walletConnected: false,
Expand Down
65 changes: 65 additions & 0 deletions examples/browser/aepp/src/components/SelectNetwork.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<h2>Select network</h2>
<div class="group">
<div>
<div>Select by</div>
<div>
<label>
<input
type="radio"
value="networkId"
v-model="mode"
>
Network ID
</label>
<label>
<input
type="radio"
value="nodeUrl"
v-model="mode"
>
Node URL
</label>
</div>
</div>
<div>
<div>Payload</div>
<div>
<input
v-model="payload"
placeholder="Network ID or node URL"
>
</div>
</div>
<button @click="() => { promise = selectNetwork(); }">
Select network
</button>
<div v-if="promise">
<div>Select network result</div>
<Value :value="promise" />
</div>
</div>
</template>

<script>
import { mapState } from 'vuex';
import Value from './Value.vue';
export default {
components: { Value },
props: {
select: { type: Function, required: true },
},
data: () => ({
mode: 'networkId',
payload: 'ae_mainnet',
promise: null,
}),
methods: {
async selectNetwork() {
await this.select({ [this.mode]: this.payload });
return 'Accepted by wallet';
},
},
};
</script>
17 changes: 16 additions & 1 deletion examples/browser/wallet-iframe/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
import {
MemoryAccount, AeSdkWallet, Node, CompilerHttp,
BrowserWindowMessageConnection, METHODS, WALLET_TYPE, RPC_STATUS,
RpcConnectionDenyError, RpcRejectedByUserError, unpackTx, unpackDelegation,
RpcConnectionDenyError, RpcRejectedByUserError, RpcNoNetworkById, unpackTx, unpackDelegation,
} from '@aeternity/aepp-sdk';
import { TypeResolver, ContractByteArrayEncoder } from '@aeternity/aepp-calldata';
import Value from './Value.vue';
Expand Down Expand Up @@ -229,6 +229,21 @@ export default {
console.log('disconnected client', clientId);
this.clientId = null;
},
onAskToSelectNetwork: async (aeppId, parameters, origin) => {
await genConfirmCallback('select network')(aeppId, parameters, origin);
if (parameters.networkId) {
if (!this.aeSdk.pool.has(parameters.networkId)) {
throw new RpcNoNetworkById(parameters.networkId);
}
await this.aeSdk.selectNode(parameters.networkId);
this.nodeName = parameters.networkId;
} else {
this.aeSdk.pool.delete('by-aepp');
this.aeSdk.addNode('by-aepp', new Node(parameters.nodeUrl));
await this.aeSdk.selectNode('by-aepp');
this.nodeName = 'by-aepp';
}
}
});
if (this.runningInFrame) this.shareWalletInfo();
Expand Down
23 changes: 17 additions & 6 deletions examples/browser/wallet-web-extension/src/background.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import browser from 'webextension-polyfill';
import {
AeSdkWallet, CompilerHttp, Node, MemoryAccount, BrowserRuntimeConnection,
WALLET_TYPE, RpcConnectionDenyError, RpcRejectedByUserError, unpackTx, unpackDelegation,
AeSdkWallet, CompilerHttp, Node, MemoryAccount, BrowserRuntimeConnection, WALLET_TYPE,
RpcConnectionDenyError, RpcRejectedByUserError, RpcNoNetworkById, unpackTx, unpackDelegation,
} from '@aeternity/aepp-sdk';
import { TypeResolver, ContractByteArrayEncoder } from '@aeternity/aepp-calldata';

Expand Down Expand Up @@ -100,10 +100,10 @@ class AccountMemoryProtected extends MemoryAccount {

const aeSdk = new AeSdkWallet({
onCompiler: new CompilerHttp('https://v8.compiler.aepps.com'),
nodes: [{
name: 'testnet',
instance: new Node('https://testnet.aeternity.io'),
}],
nodes: [
{ name: 'ae_uat', instance: new Node('https://testnet.aeternity.io') },
{ name: 'ae_mainnet', instance: new Node('https://mainnet.aeternity.io') },
],
accounts: [
new AccountMemoryProtected('sk_2CuofqWZHrABCrM7GY95YSQn8PyFvKQadnvFnpwhjUnDCFAWmf'),
AccountMemoryProtected.generate(),
Expand All @@ -126,6 +126,17 @@ const aeSdk = new AeSdkWallet({
},
onSubscription: genConfirmCallback('subscription'),
onAskAccounts: genConfirmCallback('get accounts'),
async onAskToSelectNetwork(aeppId, parameters, origin) {
await genConfirmCallback('select network')(aeppId, parameters, origin);
if (parameters.networkId) {
if (!this.pool.has(parameters.networkId)) throw new RpcNoNetworkById(parameters.networkId);
await this.selectNode(parameters.networkId);
} else {
this.pool.delete('by-aepp');
this.addNode('by-aepp', new Node(parameters.nodeUrl));
await this.selectNode('by-aepp');
}
},
});
// The `ExtensionProvider` uses the first account by default.
// You can change active account using `selectAccount(address)` function
Expand Down
9 changes: 0 additions & 9 deletions examples/browser/wallet-web-extension/vue.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ module.exports = {
},
},
},
// required for `instanceof RestError`
configureWebpack: {
resolve: {
alias: {
'@azure/core-client': '@azure/core-client/dist/browser/index.js',
'@azure/core-rest-pipeline': '@azure/core-rest-pipeline/dist/browser/index.js',
},
},
},
// this workaround is only needed when sdk is not in the node_modules folder
chainWebpack: (config) => {
const sdkPath = path.join(__dirname, '..', '..', '..', 'es');
Expand Down
10 changes: 9 additions & 1 deletion src/AeSdkAepp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import AccountBase from './account/Base';
import AccountRpc from './account/Rpc';
import { decode, Encoded } from './utils/encoder';
import {
Accounts, RPC_VERSION, WalletInfo, Network, WalletApi, AeppApi, Node as NodeRpc,
Accounts, RPC_VERSION, WalletInfo, Network, WalletApi, AeppApi, Node as NodeRpc, NetworkToSelect,
} from './aepp-wallet-communication/rpc/types';
import RpcClient from './aepp-wallet-communication/rpc/RpcClient';
import { METHODS, SUBSCRIPTION_TYPES } from './aepp-wallet-communication/schema';
Expand Down Expand Up @@ -167,6 +167,14 @@ export default class AeSdkAepp extends AeSdkBase {
return result;
}

/**
* Ask wallet to select a network
*/
async askToSelectNetwork(network: NetworkToSelect): Promise<void> {
this._ensureConnected();
await this.rpcClient.request(METHODS.updateNetwork, network);
}

_ensureConnected(): asserts this is AeSdkAepp & { rpcClient: NonNullable<AeSdkAepp['rpcClient']> } {
if (this.rpcClient != null) return;
throw new NoWalletConnectedError('You are not connected to Wallet');
Expand Down
17 changes: 17 additions & 0 deletions src/AeSdkWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Accounts,
AeppApi,
Network,
NetworkToSelect,
RPC_VERSION,
WalletApi,
WalletInfo,
Expand Down Expand Up @@ -41,6 +42,10 @@ type OnAskAccounts = (
clientId: string, params: undefined, origin: string
) => void;

type OnAskToSelectNetwork = (
clientId: string, params: NetworkToSelect, origin: string
) => void;

interface RpcClientsInfo {
id: string;
status: RPC_STATUS;
Expand Down Expand Up @@ -70,6 +75,8 @@ export default class AeSdkWallet extends AeSdk {

onAskAccounts: OnAskAccounts;

onAskToSelectNetwork: OnAskToSelectNetwork;

/**
* @param options - Options
* @param options.name - Wallet name
Expand All @@ -78,6 +85,8 @@ export default class AeSdkWallet extends AeSdk {
* @param options.onConnection - Call-back function for incoming AEPP connection
* @param options.onSubscription - Call-back function for incoming AEPP account subscription
* @param options.onAskAccounts - Call-back function for incoming AEPP get address request
* @param options.onAskToSelectNetwork - Call-back function for incoming AEPP select network
* request. If the request is fine then this function should change the current network.
* @param options.onDisconnect - Call-back function for disconnect event
*/
constructor({
Expand All @@ -88,6 +97,7 @@ export default class AeSdkWallet extends AeSdk {
onSubscription,
onDisconnect,
onAskAccounts,
onAskToSelectNetwork,
...options
}: {
id: string;
Expand All @@ -97,12 +107,14 @@ export default class AeSdkWallet extends AeSdk {
onSubscription: OnSubscription;
onDisconnect: OnDisconnect;
onAskAccounts: OnAskAccounts;
onAskToSelectNetwork: OnAskToSelectNetwork;
} & ConstructorParameters<typeof AeSdk>[0]) {
super(options);
this.onConnection = onConnection;
this.onSubscription = onSubscription;
this.onDisconnect = onDisconnect;
this.onAskAccounts = onAskAccounts;
this.onAskToSelectNetwork = onAskToSelectNetwork;
this.name = name;
this.id = id;
this._type = type;
Expand Down Expand Up @@ -317,6 +329,11 @@ export default class AeSdkWallet extends AeSdk {
const signature = await this.signDelegation(delegation, parameters);
return { signature };
},
[METHODS.updateNetwork]: async (params, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
await this.onAskToSelectNetwork(id, params, origin);
return null;
},
},
),
};
Expand Down
9 changes: 8 additions & 1 deletion src/aepp-wallet-communication/WalletConnectorFrameBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import EventEmitter from 'eventemitter3';
import AccountRpc from '../account/Rpc';
import { Encoded } from '../utils/encoder';
import {
Accounts, RPC_VERSION, Network, WalletApi, AeppApi,
Accounts, RPC_VERSION, Network, WalletApi, AeppApi, NetworkToSelect,
} from './rpc/types';
import RpcClient from './rpc/RpcClient';
import { METHODS, SUBSCRIPTION_TYPES } from './schema';
Expand Down Expand Up @@ -116,4 +116,11 @@ export default abstract class WalletConnectorFrameBase<T extends {}>
this.#updateAccounts(result.address);
return this.#accounts;
}

/**
* Ask wallet to select a network
*/
async askToSelectNetwork(network: NetworkToSelect): Promise<void> {
await this.#getRpcClient().request(METHODS.updateNetwork, network);

Check warning on line 124 in src/aepp-wallet-communication/WalletConnectorFrameBase.ts

View check run for this annotation

Codecov / codecov/patch

src/aepp-wallet-communication/WalletConnectorFrameBase.ts#L124

Added line #L124 was not covered by tests
}
}
4 changes: 4 additions & 0 deletions src/aepp-wallet-communication/rpc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type Icons = Array<{ src: string; sizes?: string; type?: string; purpose?: strin

export const RPC_VERSION = 1;

export type NetworkToSelect = { networkId: string } | { nodeUrl: string };

export interface WalletApi {
[METHODS.connect]: (
p: { name: string; icons?: Icons; version: typeof RPC_VERSION; connectNode: boolean }
Expand Down Expand Up @@ -92,6 +94,8 @@ export interface WalletApi {
onAccount: Encoded.AccountAddress;
},
) => Promise<{ signature: Encoded.Signature }>;

[METHODS.updateNetwork]: (a: NetworkToSelect) => Promise<null>;
}

export interface AeppApi {
Expand Down
16 changes: 16 additions & 0 deletions src/aepp-wallet-communication/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,22 @@ export class RpcInternalError extends RpcError {
}
rpcErrors.push(RpcInternalError);

/**
* @category exception
*/
export class RpcNoNetworkById extends RpcError {
static override code = 13;

override code = 13;

constructor(networkId: string) {
super(`Wallet can't find a network by id "${networkId}"`);
this.data = networkId;
this.name = 'RpcNoNetworkById';
}
}
rpcErrors.push(RpcNoNetworkById);

/**
* @category exception
*/
Expand Down
2 changes: 1 addition & 1 deletion src/index-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export {
MESSAGE_DIRECTION, WALLET_TYPE, SUBSCRIPTION_TYPES, METHODS, RPC_STATUS, RpcError,
RpcInvalidTransactionError, RpcRejectedByUserError, RpcUnsupportedProtocolError,
RpcConnectionDenyError, RpcNotAuthorizeError, RpcPermissionDenyError, RpcInternalError,
RpcMethodNotFoundError,
RpcMethodNotFoundError, RpcNoNetworkById,
} from './aepp-wallet-communication/schema';
export { default as walletDetector } from './aepp-wallet-communication/wallet-detector';
export { default as BrowserRuntimeConnection } from './aepp-wallet-communication/connection/BrowserRuntime';
Expand Down
28 changes: 28 additions & 0 deletions test/integration/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CompilerHttp,
RpcConnectionDenyError,
RpcRejectedByUserError,
RpcNoNetworkById,
SUBSCRIPTION_TYPES,
Tag,
WALLET_TYPE,
Expand Down Expand Up @@ -111,6 +112,7 @@ describe('Aepp<->Wallet', () => {
onConnection: handlerReject,
onSubscription: handlerReject,
onAskAccounts: handlerReject,
onAskToSelectNetwork: handlerReject,
onDisconnect() {},
});
aepp = new AeSdkAepp({
Expand Down Expand Up @@ -522,6 +524,31 @@ describe('Aepp<->Wallet', () => {
});
});

describe('Select network by aepp', () => {
it('rejected by user on wallet side', async () => {
wallet.onAskToSelectNetwork = () => { throw new RpcRejectedByUserError(); };
await expect(aepp.askToSelectNetwork({ networkId: 'ae_test' })).to.be.eventually
.rejectedWith('Operation rejected by user').with.property('code', 4);
wallet.onAskToSelectNetwork = handlerReject;
});

it('rejected because unknown network id', async () => {
wallet.onAskToSelectNetwork = () => { throw new RpcNoNetworkById('test'); };
await expect(aepp.askToSelectNetwork({ networkId: 'ae_test' })).to.be.eventually
.rejectedWith('Wallet can\'t find a network by id "test"').with.property('code', 13);
wallet.onAskToSelectNetwork = handlerReject;
});

it('works', async () => {
let args: unknown[] = [];
wallet.onAskToSelectNetwork = (...a) => { args = a; };
const payload = { nodeUrl: 'http://example.com' };
await aepp.askToSelectNetwork(payload);
wallet.onAskToSelectNetwork = handlerReject;
expect(args.slice(1)).to.be.eql([payload, 'http://origin.test']);
});
});

it('Sign and broadcast invalid transaction', async () => {
const tx = await aepp.buildTx({
tag: Tag.SpendTx,
Expand Down Expand Up @@ -650,6 +677,7 @@ describe('Aepp<->Wallet', () => {
onConnection() {},
onSubscription: handlerReject,
onAskAccounts: handlerReject,
onAskToSelectNetwork: handlerReject,
onDisconnect() {},
});
aepp = new AeSdkAepp({
Expand Down

0 comments on commit 9871c91

Please sign in to comment.