Skip to content

Commit acd86d9

Browse files
committed
feat: add wallet_addEthereumChain functionality & switch chain fixes
- Add support for wallet_addEthereumChain when switch chain fails - Refactor user rejection handling and apply to wallet_switchEthereumChain - Always call wallet_switchEthereumChain to workaround metamask returning wrong chain in eth_chainId when the chain is switched from Metamask
1 parent d0b837f commit acd86d9

4 files changed

Lines changed: 94 additions & 42 deletions

File tree

javascript/engine-js/src/TokenScript.ts

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -798,45 +798,29 @@ export class TokenScript {
798798
*/
799799
public async executeTransaction(transaction: Transaction, listener?: ITransactionListener, waitForConfirmation: boolean = true){
800800

801-
try {
802-
803-
const wallet = await this.engine.getWalletAdapter();
804-
const transInfo = transaction.getTransactionInfo();
805-
806-
// TODO: confirm with James exact use cases of having multiple address in a contract element
807-
const chain = this.getCurrentTokenContext()?.chainId ?? await wallet.getChain();
808-
const contract = transInfo.contract.getAddressByChain(chain, true);
801+
const wallet = await this.engine.getWalletAdapter();
802+
const transInfo = transaction.getTransactionInfo();
809803

810-
// If validation callback returns false we abort silently
811-
if (!await this.transactionValidator.validateContract(chain, contract.address, transInfo.contract, transInfo.function))
812-
return false;
804+
// TODO: confirm with James exact use cases of having multiple address in a contract element
805+
const chain = this.getCurrentTokenContext()?.chainId ?? await wallet.getChain();
806+
const contract = transInfo.contract.getAddressByChain(chain, true);
813807

814-
const errorAbi = transInfo.contract.getAbi("error");
808+
// If validation callback returns false we abort silently
809+
if (!await this.transactionValidator.validateContract(chain, contract.address, transInfo.contract, transInfo.function))
810+
return false;
815811

816-
const ethParams = [];
817-
818-
for (let i in transInfo.args){
819-
ethParams.push(await transInfo.args[i].getEthersArgument(this.getCurrentTokenContext()))
820-
}
812+
const errorAbi = transInfo.contract.getAbi("error");
821813

822-
const ethValue = transInfo.value ? await transInfo?.value?.getValue(this.getCurrentTokenContext()) : null;
814+
const ethParams = [];
823815

824-
listener({status: 'started'});
825-
826-
await wallet.sendTransaction(contract.chain, contract.address, transInfo.function, ethParams, [], ethValue, waitForConfirmation, listener, errorAbi);
827-
828-
return true;
816+
for (let i in transInfo.args){
817+
ethParams.push(await transInfo.args[i].getEthersArgument(this.getCurrentTokenContext()))
818+
}
829819

830-
} catch (e){
820+
const ethValue = transInfo.value ? await transInfo?.value?.getValue(this.getCurrentTokenContext()) : null;
831821

832-
if (
833-
e.message.indexOf("ACTION_REJECTED") > -1 ||
834-
e.message.indexOf("Rejected by the user") > -1 ||
835-
e.message.indexOf("User denied") > -1
836-
)
837-
throw new Error("Transaction rejected");
822+
listener({status: 'started'});
838823

839-
throw e;
840-
}
824+
return await wallet.sendTransaction(contract.chain, contract.address, transInfo.function, ethParams, [], ethValue, waitForConfirmation, listener, errorAbi);
841825
}
842826
}

javascript/engine-js/src/tokenScript/Card.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ export class Card {
185185

186186
const processed = await this.tokenScript.executeTransaction(transaction, listener, waitForConfirmation);
187187

188-
if (!processed)
188+
// User rejection
189+
if (processed === false)
189190
return;
190191

191192
// TODO: transactions should specify which attributes should be invalidated

javascript/engine-js/src/wallet/EthersAdapter.ts

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import {IWalletAdapter, RpcRequest} from "./IWalletAdapter";
2-
import {Contract, ContractRunner, ContractTransaction, ethers, EventLog, FetchRequest, Network} from "ethers";
2+
import {
3+
Contract,
4+
ContractRunner,
5+
ContractTransaction,
6+
ethers,
7+
EventLog,
8+
getBigInt,
9+
Network, Overrides
10+
} from "ethers";
311
import {ITransactionListener} from "../TokenScript";
412
import {ErrorDecoder, ErrorType} from "ethers-decode-error";
513

@@ -48,18 +56,24 @@ export class EthersAdapter implements IWalletAdapter {
4856
return (await contract.getFunction(method).staticCall(...(args.map((arg: any) => arg.value))));
4957
}
5058

51-
async sendTransaction(chain: number, contractAddr: string, method: string, args: any[], outputTypes: string[], value?: BigInt, waitForConfirmation: boolean = true, listener?: ITransactionListener, errorAbi: any[] = []){
59+
async sendTransaction(chain: number, contractAddr: string, method: string, args: any[], outputTypes: string[], value?: bigint, waitForConfirmation: boolean = true, listener?: ITransactionListener, errorAbi: any[] = []){
5260

5361
console.log("Send ethereum transaction. chain " + chain + "; contract " + contractAddr + "; method " + method + "; value " + value + "; args", args);
5462

55-
await this.switchChain(chain);
63+
const res = await this.switchChain(chain);
64+
65+
// User rejection
66+
if (!res)
67+
return false;
5668

5769
// TODO: if no method is set, send raw transaction? Is this allowed?
5870
// TODO: handle no-method transaction
5971

6072
const contract = await this.getEthersContractInstance(chain, contractAddr, method, args, outputTypes, value ? "payable" : "nonpayable", errorAbi);
6173

62-
const overrides: any = {};
74+
const overrides: Overrides = {
75+
chainId: chain
76+
};
6377

6478
if (value)
6579
overrides.value = value;
@@ -72,6 +86,10 @@ export class EthersAdapter implements IWalletAdapter {
7286
try {
7387
tx = await contract[method](...(args.map((arg: any) => arg.value)), overrides) as ContractTransaction;
7488
} catch (e: any){
89+
90+
if (EthersAdapter.isTransactionRejection(e))
91+
return false;
92+
7593
const errDecoder = ErrorDecoder.create(errorAbi)
7694
const decodedError = await errDecoder.decode(e);
7795
console.error(e);
@@ -129,6 +147,14 @@ export class EthersAdapter implements IWalletAdapter {
129147
return tx;
130148
}
131149

150+
private static isTransactionRejection(e: any){
151+
return (
152+
e.message.indexOf("ACTION_REJECTED") > -1 ||
153+
e.message.indexOf("Rejected by the user") > -1 ||
154+
e.message.indexOf("User denied") > -1
155+
);
156+
}
157+
132158
private async getEthersContractInstance(chain: number, contractAddr: string, method: string, args: any[], outputTypes: string[]|any[], stateMutability: string, errorAbi: any[] = []){
133159

134160
const abiData = {
@@ -187,21 +213,62 @@ export class EthersAdapter implements IWalletAdapter {
187213
return Number(network.chainId);
188214
}
189215

190-
private async switchChain(chain: number){
216+
private async switchChain(chain: number, tryToAdd = true): Promise<boolean> {
191217

192218
const ethersProvider = await this.getEthersProvider();
193219

194-
if (chain != await this.getChain()){
220+
// TODO: Metamask is currently returning an old chain ID when chain is switched via the metamask UI, so we need to call this everytime
221+
//console.log("Current chain: ", await this.getChain());
222+
//if (chain != await this.getChain()){
195223

196224
console.log("Switch chain: ", chain);
197225

198226
try {
199227
await ethersProvider.send("wallet_switchEthereumChain", [{chainId: "0x" + chain.toString(16)}]);
228+
return true;
200229
} catch (e){
201230
console.error(e);
231+
if (EthersAdapter.isTransactionRejection(e))
232+
return false;
233+
234+
if (tryToAdd){
235+
await this.addChain(chain);
236+
return this.switchChain(chain, false);
237+
}
202238
throw new Error("Connected to wrong chain, please switch the chain to chainId: " + chain + ", error: " + e.message);
203239
}
204-
}
240+
//}
241+
}
242+
243+
private async addChain(chain: number){
244+
console.info("Trying to add ethereumChain");
245+
246+
if (!this.chainConfig[chain])
247+
throw new Error("Chain is not defined in TS engine config");
248+
249+
const chainList = await fetch("https://chainid.network/chains.json").then((res) => res.json()) as any[];
250+
251+
const chainMeta = chainList.find((meta) => meta.chainId === chain);
252+
253+
if (!chainMeta)
254+
throw new Error("Could not find chain config at chainid.network");
255+
256+
const chainIcons = await fetch("https://chainid.network/chain_icons.json").then((res) => res.json()) as any[];
257+
258+
const icon = chainIcons.find((meta) => meta.name === chainMeta.icon);
259+
260+
// TODO: Convert IPFS URLs
261+
const icons = icon ? [icon.icons[0].url] : [];
262+
263+
const ethersProvider = await this.getEthersProvider();
264+
await ethersProvider.send("wallet_addEthereumChain", [{
265+
chainId: "0x" + chain.toString(16),
266+
chainName: chainMeta.name,
267+
iconUrls: icons,
268+
rpcUrls: chainMeta.rpc.filter(url => url.indexOf("${INFURA_API_KEY}") === -1),
269+
nativeCurrency: chainMeta.nativeCurrency,
270+
blockExplorerUrls: chainMeta.explorers ? chainMeta.explorers.map(explorer => explorer.url) : []
271+
}]);
205272
}
206273

207274
async getCurrentWalletAddress() {

javascript/engine-js/src/wallet/IWalletAdapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ export interface IWalletAdapter {
4343
method: string,
4444
args: any[],
4545
outputTypes: string[],
46-
value?: BigInt,
46+
value?: bigint,
4747
waitForConfirmation?: boolean,
4848
listener?: ITransactionListener,
4949
errorAbi?: any[]
50-
): Promise<any>;
50+
): Promise<any|false>;
5151
getChain(): Promise<number>;
5252
getRpcUrls(chainId: number): string[];
5353
rpcProxy(request: RpcRequest): Promise<any>;

0 commit comments

Comments
 (0)