Skip to content

Commit

Permalink
fix: react-wallet-v2 fixed for Tezos
Browse files Browse the repository at this point in the history
  • Loading branch information
dianasavvatina committed Sep 10, 2024
1 parent f3256d9 commit 138a6b4
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 126 deletions.
10 changes: 5 additions & 5 deletions advanced/wallets/react-wallet-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ Your `.env.local` now contains the following environment variables:

## Navigating through example

1. Initial setup and initializations happen in [_app.ts](https://github.com/WalletConnect/web-examples/blob/main/wallets/react-wallet-v2/src/pages/_app.tsx) file
2. WalletConnect client, ethers and cosmos wallets are initialized in [useInitialization.ts ](https://github.com/WalletConnect/web-examples/blob/main/wallets/react-wallet-v2/src/hooks/useInitialization.ts) hook
3. Subscription and handling of WalletConnect events happens in [useWalletConnectEventsManager.ts](https://github.com/WalletConnect/web-examples/blob/main/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts) hook, that opens related [Modal views](https://github.com/WalletConnect/web-examples/tree/main/wallets/react-wallet-v2/src/views) and passes them all necessary data
4. [Modal views](https://github.com/WalletConnect/web-examples/tree/main/wallets/react-wallet-v2/src/views) are responsible for data display and handling approval or rejection actions
5. Upon approval or rejection, modals pass the request data to [RequestHandlerUtil.ts](https://github.com/WalletConnect/web-examples/blob/main/wallets/react-wallet-v2/src/utils/RequestHandlerUtil.ts) that performs all necessary work based on the request method and returns formated json rpc result data that can be then used for WalletConnect client responses
1. Initial setup and initializations happen in [_app.ts](https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/pages/_app.tsx) file
2. WalletConnect client, ethers and cosmos wallets are initialized in [useInitialization.ts ](https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts) hook
3. Subscription and handling of WalletConnect events happens in [useWalletConnectEventsManager.ts](https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts) hook, that opens related [Modal views](https://github.com/WalletConnect/web-examples/tree/main/wallets/react-wallet-v2/src/views) and passes them all necessary data
4. [Modal views](https://github.com/WalletConnect/web-examples/tree/main/advanced/wallets/react-wallet-v2/src/views) are responsible for data display and handling approval or rejection actions
5. Upon approval or rejection, modals pass the request data to [RequestHandlerUtil.ts](https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/utils/RequestHandlerUtil.ts) that performs all necessary work based on the request method and returns formated json rpc result data that can be then used for WalletConnect client responses

## Preview of wallet and dapp examples in action

Expand Down
6 changes: 4 additions & 2 deletions advanced/wallets/react-wallet-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"prettier:write": "prettier --write '**/*.{js,ts,jsx,tsx}'"
},
"dependencies": {
"@airgap/beacon-types": "^4.2.2",
"@biconomy/permission-context-builder": "^1.0.9",
"@cosmjs/amino": "0.32.3",
"@cosmjs/encoding": "0.32.3",
Expand All @@ -31,8 +32,9 @@
"@polkadot/types": "^9.3.3",
"@rhinestone/module-sdk": "0.1.2",
"@solana/web3.js": "1.89.2",
"@taquito/signer": "^15.1.0",
"@taquito/taquito": "^15.1.0",
"@taquito/rpc": "^20.0.1",
"@taquito/signer": "^20.0.1",
"@taquito/taquito": "^20.0.1",
"@types/semver": "^7.5.8",
"@walletconnect/web3wallet": "1.15.1",
"@zerodev/ecdsa-validator": "5.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function ModalFooter({
auto
flat
style={{ color: 'white', backgroundColor: 'grey' }}
onPress={onReject}
onClick={onReject}
data-testid="session-reject-button"
disabled={disableReject || rejectLoader?.active}
>
Expand All @@ -68,7 +68,7 @@ export default function ModalFooter({
flat
color={approveButtonColor}
disabled={disableApprove || approveLoader?.active}
onPress={onApprove}
onClick={onApprove}
data-testid="session-approve-button"
>
{approveLoader && approveLoader.active ? (
Expand Down
5 changes: 3 additions & 2 deletions advanced/wallets/react-wallet-v2/src/data/TezosData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type ChainMetadata = {
rgb: string
rpc: string
namespace: string
api?: string
}

/**
Expand All @@ -21,7 +22,7 @@ export const TEZOS_MAINNET_CHAINS: Record<string, ChainMetadata> = {
name: 'Tezos',
logo: '/chain-logos/tezos.svg',
rgb: '44, 125, 247',
rpc: 'https://mainnet.api.tez.ie',
rpc: 'https://rpc.tzbeta.net',
namespace: 'tezos'
}
}
Expand All @@ -32,7 +33,7 @@ export const TEZOS_TEST_CHAINS: Record<string, ChainMetadata> = {
name: 'Tezos Testnet',
logo: '/chain-logos/tezos.svg',
rgb: '44, 125, 247',
rpc: 'https://ghostnet.ecadinfra.com',
rpc: 'https://rpc.ghostnet.teztnets.com',
namespace: 'tezos'
}
}
Expand Down
207 changes: 180 additions & 27 deletions advanced/wallets/react-wallet-v2/src/lib/TezosLib.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { TezosToolkit } from '@taquito/taquito'
import { OpKind, ParamsWithKind, TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer'
import { localForger } from '@taquito/local-forging'

import { Wallet } from 'ethers/'
import { PvmKind, ScriptedContracts } from '@taquito/rpc';
import { PartialTezosOperation, TezosOperationType } from '@airgap/beacon-types'
import { TEZOS_CHAINS } from '@/data/TezosData';

/**
* Constants
Expand All @@ -22,7 +25,7 @@ interface IInitArguments {
* Library
*/
export default class TezosLib {
tezos: TezosToolkit
toolkits: Record<string, TezosToolkit>
signer: InMemorySigner
mnemonic: string
secretKey: string
Expand All @@ -31,15 +34,15 @@ export default class TezosLib {
curve: 'ed25519' | 'secp256k1'

constructor(
tezos: TezosToolkit,
toolkits: Record<string, TezosToolkit>,
mnemonic: string,
signer: InMemorySigner,
secretKey: string,
publicKey: string,
address: string,
curve: 'ed25519' | 'secp256k1'
) {
this.tezos = tezos
this.toolkits = toolkits
this.mnemonic = mnemonic
this.signer = signer
this.secretKey = secretKey
Expand All @@ -55,17 +58,27 @@ export default class TezosLib {
curve: curve ?? DEFAULT_CURVE
}

const Tezos = new TezosToolkit('https://mainnet.api.tez.ie')

const signer = InMemorySigner.fromMnemonic(params)

Tezos.setSignerProvider(signer)
const signer = InMemorySigner.fromMnemonic(params);

const secretKey = await signer.secretKey()
const publicKey = await signer.publicKey()
const address = await signer.publicKeyHash()
// we have wallets for multiple networks
// later we will determine the chain from the request
const toolkits: Record<string, TezosToolkit> = {};
for (const chainKey in TEZOS_CHAINS) {
const chain = TEZOS_CHAINS[chainKey];
const toolkit = new TezosToolkit(chain.rpc);

toolkit.setSignerProvider(signer);
toolkits[chainKey] = toolkit;
}
const toolkit = toolkits['tezos:mainnet'];
const secretKey = await toolkit.signer.secretKey();
const publicKey = await toolkit.signer.publicKey();
const address = await toolkit.signer.publicKeyHash();

return new TezosLib(Tezos, params.mnemonic, signer, secretKey, publicKey, address, params.curve)
if (!secretKey) {
throw new Error("Failed to generate secret key");
}
return new TezosLib(toolkits, params.mnemonic, signer, secretKey, publicKey, address, params.curve)
}

public getMnemonic() {
Expand All @@ -84,23 +97,163 @@ export default class TezosLib {
return this.address
}

public async signTransaction(transaction: any) {
const prepared = await this.tezos.prepare.batch(
transaction.map((tx: any) => ({
amount: tx.amount,
to: tx.destination,
kind: tx.kind,
mutez: true
}))
)
public convertToPartialParamsWithKind(op: PartialTezosOperation): ParamsWithKind {
switch (op.kind) {
case TezosOperationType.ACTIVATE_ACCOUNT:
return {
kind: OpKind.ACTIVATION,
pkh: op.pkh,
secret: op.secret,
};
case TezosOperationType.DELEGATION:
return {
kind: OpKind.DELEGATION,
source: op.source ?? "source not provided",
delegate: op.delegate,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
};
case TezosOperationType.FAILING_NOOP:
return {
kind: OpKind.FAILING_NOOP,
arbitrary: op.arbitrary,
basedOnBlock: 'head',
};
case TezosOperationType.INCREASE_PAID_STORAGE:
return {
kind: OpKind.INCREASE_PAID_STORAGE,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
amount: Number(op.amount),
destination: op.destination,
};
case TezosOperationType.ORIGINATION:
let script : ScriptedContracts = op.script as unknown as ScriptedContracts;
return {
kind: OpKind.ORIGINATION,
balance: Number(op.balance),
code: script.code,
init: script.storage,
delegate: op.delegate,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
};
case TezosOperationType.REGISTER_GLOBAL_CONSTANT:
return {
kind: OpKind.REGISTER_GLOBAL_CONSTANT,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
value: op.value,
};
case TezosOperationType.SMART_ROLLUP_ADD_MESSAGES:
return {
kind: OpKind.SMART_ROLLUP_ADD_MESSAGES,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
message: op.message,
};
case TezosOperationType.SMART_ROLLUP_ORIGINATE:
if (!Object.values(PvmKind).includes(op.pvm_kind)) {
throw new Error(`Invalid PvmKind: ${op.pvm_kind}`);
}
return {
kind: OpKind.SMART_ROLLUP_ORIGINATE,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
pvmKind: op.pvm_kind,
kernel: op.kernel,
parametersType: op.parameters_ty,
};
case TezosOperationType.SMART_ROLLUP_EXECUTE_OUTBOX_MESSAGE:
return {
kind: OpKind.SMART_ROLLUP_EXECUTE_OUTBOX_MESSAGE,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
rollup: op.rollup,
cementedCommitment: op.cemented_commitment,
outputProof: op.output_proof,
};
case TezosOperationType.TRANSACTION:
return {
kind: OpKind.TRANSACTION,
to: op.destination,
amount: Number(op.amount),
mutez: true,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
parameter: op.parameters,
};
case TezosOperationType.TRANSFER_TICKET:
return {
kind: OpKind.TRANSFER_TICKET,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
ticketContents: op.ticket_contents,
ticketTy: op.ticket_ty,
ticketTicketer: op.ticket_ticketer,
ticketAmount: Number(op.ticket_amount),
destination: op.destination,
entrypoint: op.entrypoint,
};
case TezosOperationType.UPDATE_CONSENSUS_KEY:
return {
kind: OpKind.UPDATE_CONSENSUS_KEY,
source: op.source,
fee: op.fee ? Number(op.fee) : undefined,
gasLimit: op.gas_limit ? Number(op.gas_limit) : undefined,
storageLimit: op.storage_limit ? Number(op.storage_limit) : undefined,
pk: op.pk,
};
default:
throw new Error(`Operation kind cannot be converted to ParamsWithKind: ${op.kind}`);
}
}

public async signTransaction(transaction: any, chainId: string) {
// Map the transactions and prepare the batch
console.log(`Wallet: handling transaction: `, transaction);
const batchTransactions: ParamsWithKind[] = transaction.map((tx: PartialTezosOperation) => {
if (tx.kind === TezosOperationType.DELEGATION && !tx.source) {
tx.source = this.address;
}
const op: ParamsWithKind = this.convertToPartialParamsWithKind(tx);
return op;
});

const toolkit = this.toolkits[chainId];
if (!toolkit) {
throw new Error(`Toolkit not found for chainId: ${chainId}`);
}

const forged = await localForger.forge(prepared.opOb)
// Prepare the batch
console.log(`Wallet: prepared batchTransactions `, batchTransactions);
const batch = toolkit.contract.batch(batchTransactions);

const tx = await this.signer.sign(forged, new Uint8Array([3]))
// Send the batch and wait for the operation hash
console.log(`Wallet: sending batch `, batch);
const operation = await batch.send();

const hash = await this.tezos.rpc.injectOperation(tx.sbytes)
// Wait for confirmation
await operation.confirmation();

return hash
console.log('Wallet: operation confirmed:', operation);
return operation.hash;
}

public async signPayload(payload: any) {
Expand Down
3 changes: 1 addition & 2 deletions advanced/wallets/react-wallet-v2/src/utils/HelperUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import toast from 'react-hot-toast'
import { COSMOS_MAINNET_CHAINS, TCosmosChain } from '@/data/COSMOSData'
import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data'
import { MULTIVERSX_CHAINS, TMultiversxChain } from '@/data/MultiversxData'
import { NEAR_CHAINS, NEAR_TEST_CHAINS, TNearChain } from '@/data/NEARData'
import { NEAR_TEST_CHAINS, TNearChain } from '@/data/NEARData'
import { POLKADOT_CHAINS, TPolkadotChain } from '@/data/PolkadotData'
import { SOLANA_CHAINS, TSolanaChain } from '@/data/SolanaData'
import { TEZOS_CHAINS, TTezosChain } from '@/data/TezosData'
import { TRON_CHAINS, TTronChain } from '@/data/TronData'
import { KADENA_CHAINS, TKadenaChain } from '@/data/KadenaData'

import { utils } from 'ethers'
import { Verify } from '@walletconnect/types'
import bs58 from 'bs58'

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ export async function approveTezosRequest(
requestEvent: SignClientTypes.EventArguments['session_request']
) {
const { params, id } = requestEvent
const { request } = params
const { request, chainId } = params
console.log("Approving Tezos request: ", request);

if (!tezosWallets || Object.keys(tezosWallets).length === 0) {
console.error("No wallets found on Approve. Try reloading the wallet page.")
return formatJsonRpcError(id, "No Tezos wallets available. See the error log on wallet");
}

const wallet = tezosWallets[request.params.account ?? Object.keys(tezosWallets)[0]]
const allWallets = Object.keys(tezosWallets).map(key => tezosWallets[key])
Expand All @@ -25,9 +31,18 @@ export async function approveTezosRequest(
)

case TEZOS_SIGNING_METHODS.TEZOS_SEND:
const sendResponse = await wallet.signTransaction(request.params.operations)

return formatJsonRpcResult(id, { hash: sendResponse })
try {
const sendResponse = await wallet.signTransaction(request.params.operations, chainId)
return formatJsonRpcResult(id, { hash: sendResponse })
} catch (error) {
if (error instanceof Error) {
console.error("Tezos_send operation failed with error: ", error.message);
return formatJsonRpcError(id, error.message);
} else {
console.error("Tezos_send operation failed with unknown error: ", error);
return formatJsonRpcError(id, 'TEZOS_SEND failed with unknown error.');
}
}

case TEZOS_SIGNING_METHODS.TEZOS_SIGN:
const signResponse = await wallet.signPayload(request.params.payload)
Expand Down
Loading

0 comments on commit 138a6b4

Please sign in to comment.