diff --git a/examples/ts/eth/push-erc20-votes-delegation-txrequest.ts b/examples/ts/eth/push-erc20-votes-delegation-txrequest.ts new file mode 100644 index 0000000000..ec9ae79655 --- /dev/null +++ b/examples/ts/eth/push-erc20-votes-delegation-txrequest.ts @@ -0,0 +1,270 @@ +/* eslint-disable no-console */ +/** + * Create a Wallet Platform tx request for EIP-712 ERC20Votes delegation (`signTypedStructuredData`) + * so it flows through policy and appears for operators in the Admin / BGMS approval UI. + * + * This does **not** sign locally: no wallet passphrase or `prv`. It only POSTs the intent BitGo + * expects (same shape as `createTxRequestWithIntentForTypedDataSigning` in sdk-core). + * + * Uses `BitGoAPI` + `@bitgo/sdk-coin-eth` only (not the full `bitgo` metapackage) to avoid loading + * every coin implementation — warnings like duplicate `@polkadot/*` can still appear from + * transitive deps; they are harmless for this script. + * + * From repo root after `yarn install`: + * + * npx tsx examples/ts/eth/push-erc20-votes-delegation-txrequest.ts + * + * Required env: + * BITGO_ACCESS_TOKEN (or ACCESS_TOKEN) + * WALLET_ID + * COIN — must match the wallet’s chain in BitGo / WP (e.g. `eth`, `teth`, `hteth`). If you see + * `Coin unsupported`, this environment does not enable that coin — try `hteth` (Holesky) + * or `eth` (mainnet) per your org. + * + * Delegation fields: + * DELEGATEE — defaults to the wallet’s receive address (self-delegation) if unset + * NONCE — EIP-712 nonce from the token’s `nonces(delegator)`; if unset, set ETH_RPC_URL and this + * script will read it on-chain (delegator = wallet receive address; token = domain.verifyingContract) + * EXPIRY — optional unix seconds; default now + 3600 + * + * Optional: + * BITGO_ENV — `test` (default) or `prod` + * ETH_RPC_URL — JSON-RPC URL matching `EIP712_DOMAIN_JSON.chainId` (required when NONCE is omitted) + * EIP712_DOMAIN_JSON — same rules as other delegation examples; required for test / non-WLFI mainnet + * TX_API_VERSION — `full` (default) or `lite` + * TXREQUEST_PREVIEW — set to `true` to preview only (skips policy / pending approval; usually **not** what you want for Admin) + * TX_COMMENT, SEQUENCE_ID, CUSTODIAN_MESSAGE_ID — forwarded on the intent if set + * + * The intent always includes `messageStandardType: EIP712` so OVC `verifyOffchainMessages` can validate + * custodial exports (TAT downloads must carry the same field on each `messages[]` entry — Wallet Platform + * should copy intent fields onto messages when creating the tx request; `createTxRequestWithIntentForTypedDataSigning` + * in sdk-core now sends this field for all typed-data tx request API flows). + * + * Custodial signing (after this script): + * Wallet Platform completes MPC for custodial / trust-held user shares using internal trust + * tooling and admin APIs (not the same as entering a wallet passphrase in the retail UI). + * If you do not see a “sign” action in your portal, use your BitGo runbook or contact BitGo + * support / CSM for the trust-operator flow for `signTypedStructuredData` on your stack. + * + * Copyright 2026, BitGo, Inc. All Rights Reserved. + */ + +import path from 'path'; + +import dotenv from 'dotenv'; + +dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); + +import { + buildErc20VotesDelegationTypedData, + encodeErc20VotesDelegationTypedDataDigestHex, + wlfiEthereumMainnetDelegationDomain, + type Erc20VotesDelegationDomain, +} from '@bitgo/abstract-eth'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { Eth, Hteth, Teth } from '@bitgo/sdk-coin-eth'; +import { MessageStandardType, type EnvironmentName } from '@bitgo/sdk-core'; +import { ethers } from 'ethers'; + +function createBitGoClient(env: EnvironmentName): BitGoAPI { + const bitgo = new BitGoAPI({ env }); + bitgo.register('eth', Eth.createInstance); + bitgo.register('teth', Teth.createInstance); + bitgo.register('hteth', Hteth.createInstance); + return bitgo; +} + +function requireEnv(name: string): string { + const v = process.env[name]; + if (!v) { + throw new Error(`Missing required environment variable: ${name}`); + } + return v; +} + +function parseDelegationDomainFromEnv(coin: string, bitgoEnv: EnvironmentName): Erc20VotesDelegationDomain { + const raw = process.env.EIP712_DOMAIN_JSON?.trim(); + if (raw) { + const d = JSON.parse(raw) as Record; + for (const k of ['name', 'version', 'chainId', 'verifyingContract']) { + if (d[k] === undefined) { + throw new Error(`EIP712_DOMAIN_JSON must include "${k}" (from token eip712Domain())`); + } + } + return { + name: String(d.name), + version: String(d.version), + chainId: Number(d.chainId), + verifyingContract: ethers.utils.getAddress(String(d.verifyingContract)), + }; + } + + if (coin === 'eth' && bitgoEnv === 'prod') { + return wlfiEthereumMainnetDelegationDomain(); + } + + throw new Error( + 'Set EIP712_DOMAIN_JSON (name, version, chainId, verifyingContract from the token `eip712Domain()`), ' + + 'or use BITGO_ENV=prod COIN=eth for WLFI mainnet defaults.' + ); +} + +const erc20VotesNoncesAbi = ['function nonces(address owner) view returns (uint256)']; + +async function fetchDelegationNonceFromChain(params: { + rpcUrl: string; + tokenAddress: string; + delegatorAddress: string; + expectedChainId: number; +}): Promise { + const provider = new ethers.providers.JsonRpcProvider(params.rpcUrl); + const network = await provider.getNetwork(); + if (network.chainId !== params.expectedChainId) { + throw new Error( + `ETH_RPC_URL points to chainId ${network.chainId}; EIP712 domain expects chainId ${params.expectedChainId}` + ); + } + const token = new ethers.Contract(params.tokenAddress, erc20VotesNoncesAbi, provider); + const n = await token.nonces(params.delegatorAddress); + return ethers.BigNumber.from(n).toString(); +} + +function rethrowIfCoinUnsupported(e: unknown, coin: string): never { + const err = e as { result?: { error?: string; name?: string } }; + if (err?.result?.name === 'CoinUnsupported' || String(err?.result?.error).includes('Coin unsupported')) { + throw new Error( + `Wallet Platform does not support coin "${coin}" for this host (Coin unsupported). ` + + `Set COIN to a chain your environment enables (examples: hteth for Holesky, eth for Ethereum mainnet, ` + + `teth for Sepolia on public BitGo test). It must match the wallet’s coin in the BitGo UI.` + ); + } + throw e; +} + +async function main(): Promise { + const accessToken = process.env.BITGO_ACCESS_TOKEN ?? process.env.ACCESS_TOKEN; + if (!accessToken) { + throw new Error('Set BITGO_ACCESS_TOKEN (or ACCESS_TOKEN)'); + } + + const env: EnvironmentName = process.env.BITGO_ENV === 'prod' ? 'prod' : 'test'; + const coin = requireEnv('COIN'); + const walletId = requireEnv('WALLET_ID'); + const expiry = process.env.EXPIRY ?? String(Math.floor(Date.now() / 1000) + 3600); + + const apiVersion = (process.env.TX_API_VERSION as 'full' | 'lite') || 'full'; + if (apiVersion !== 'full' && apiVersion !== 'lite') { + throw new Error('TX_API_VERSION must be "full" or "lite"'); + } + const preview = process.env.TXREQUEST_PREVIEW === 'true'; + + const domain = parseDelegationDomainFromEnv(coin, env); + + const bitgo = createBitGoClient(env); + await bitgo.authenticateWithAccessToken({ accessToken }); + + let wallet; + try { + wallet = await bitgo.coin(coin).wallets().get({ id: walletId }); + } catch (e) { + rethrowIfCoinUnsupported(e, coin); + } + + const delegator = wallet.receiveAddress(); + if (!delegator) { + throw new Error( + 'Wallet has no receiveAddress yet; create or fund an address first, or set DELEGATEE / NONCE manually.' + ); + } + + const delegatee = process.env.DELEGATEE?.trim() || delegator; + + let nonce = process.env.NONCE?.trim(); + if (!nonce) { + const rpcUrl = process.env.ETH_RPC_URL?.trim(); + if (!rpcUrl) { + throw new Error( + 'Set NONCE (decimal from token `nonces(delegator)` on-chain), or omit NONCE and set ETH_RPC_URL ' + + 'so this script can call `nonces` on domain.verifyingContract for the wallet receive address.' + ); + } + nonce = await fetchDelegationNonceFromChain({ + rpcUrl, + tokenAddress: domain.verifyingContract, + delegatorAddress: ethers.utils.getAddress(delegator), + expectedChainId: domain.chainId, + }); + console.log('Fetched EIP-712 nonce from token nonces(delegator):', nonce, { + delegator, + token: domain.verifyingContract, + }); + } + + const typedDataObject = buildErc20VotesDelegationTypedData({ + domain, + message: { delegatee, nonce, expiry }, + }); + const typedDataRaw = JSON.stringify(typedDataObject); + const messageEncoded = encodeErc20VotesDelegationTypedDataDigestHex(typedDataObject); + + const intent: Record = { + intentType: 'signTypedStructuredData', + isTss: true, + messageRaw: typedDataRaw, + messageEncoded, + messageStandardType: MessageStandardType.EIP712, + }; + if (process.env.TX_COMMENT) { + intent.comment = process.env.TX_COMMENT; + } + if (process.env.SEQUENCE_ID) { + intent.sequenceId = process.env.SEQUENCE_ID; + } + if (process.env.CUSTODIAN_MESSAGE_ID) { + intent.custodianMessageId = process.env.CUSTODIAN_MESSAGE_ID; + } + + const body = { + intent, + apiVersion, + preview, + }; + + console.log('POST', `/api/v2/wallet/${walletId}/txrequests`, { apiVersion, preview, coin }); + console.log('Delegator (wallet receive address):', delegator); + console.log('Delegatee:', delegatee); + console.log('intent.intentType:', intent.intentType); + console.log('intent.messageEncoded length (hex chars):', messageEncoded.length); + + const txRequest = (await bitgo + .post(bitgo.url(`/wallet/${walletId}/txrequests`, 2)) + .send(body) + .result()) as Record; + + console.log(''); + console.log('Tx request created:', JSON.stringify(txRequest, null, 2)); + console.log(''); + if (preview) { + console.log('Note: TXREQUEST_PREVIEW=true — this was a preview; it may not create a pending approval in Admin.'); + } else { + console.log( + 'Next: open Admin for this wallet or enterprise and look up this tx request / linked pending approval.' + ); + console.log('If nothing appears, check enterprise policy, wallet permissions, and that the wallet is TSS on', coin); + } + + console.log(''); + console.log( + 'Custodial TSS: completing this sign usually requires trust / BitGo-operator tools (MPC rounds), ' + + 'not a wallet passphrase in the customer UI. Ask your BitGo contact for the procedure on your environment.' + ); + console.log( + 'Also align EIP-712 domain.chainId with the chain your wallet uses (your payload used mainnet domain ' + + 'fields while the tx request message may show a test coin like hteth — fix domain vs COIN if signing fails).' + ); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/modules/abstract-eth/src/lib/eip712/erc20VotesDelegation.ts b/modules/abstract-eth/src/lib/eip712/erc20VotesDelegation.ts new file mode 100644 index 0000000000..3293e48bc6 --- /dev/null +++ b/modules/abstract-eth/src/lib/eip712/erc20VotesDelegation.ts @@ -0,0 +1,142 @@ +import { MessageTypes, SignTypedDataVersion, TypedDataUtils, TypedMessage } from '@metamask/eth-sig-util'; +import { ethers } from 'ethers'; + +/** + * EIP-712 typed data for OpenZeppelin ERC20Votes-style `delegateBySig`: + * `Delegation(address delegatee,uint256 nonce,uint256 expiry)` + * + * Domain fields must match the token's on-chain `eip712Domain()` (e.g. WLFI proxy + * `0xda5e1988097297dcdc1f90d4dfe7909e847cbef6` on Ethereum mainnet). + */ + +const delegateBySigAbi = [ + 'function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)', +]; + +const erc20VotesDelegationTypes: Record> = { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Delegation: [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + ], +}; + +export interface Erc20VotesDelegationDomain { + name: string; + version: string; + chainId: number; + verifyingContract: string; +} + +export interface Erc20VotesDelegationMessage { + delegatee: string; + nonce: ethers.BigNumberish; + expiry: ethers.BigNumberish; +} + +export interface Erc20VotesDelegationTypedData { + types: typeof erc20VotesDelegationTypes; + primaryType: 'Delegation'; + domain: Erc20VotesDelegationDomain; + message: { + delegatee: string; + nonce: string; + expiry: string; + }; +} + +function uint256FieldToDecimalString(value: ethers.BigNumberish): string { + return ethers.BigNumber.from(value).toString(); +} + +/** + * Build EIP-712 v4 typed data for `delegateBySig` signing (e.g. BitGo `signTypedData` with + * `typedDataRaw: JSON.stringify(result)` and `SignTypedDataVersion.V4`). + */ +export function buildErc20VotesDelegationTypedData(params: { + domain: Erc20VotesDelegationDomain; + message: Erc20VotesDelegationMessage; +}): Erc20VotesDelegationTypedData { + return { + types: erc20VotesDelegationTypes, + primaryType: 'Delegation', + domain: { ...params.domain }, + message: { + delegatee: ethers.utils.getAddress(params.message.delegatee), + nonce: uint256FieldToDecimalString(params.message.nonce), + expiry: uint256FieldToDecimalString(params.message.expiry), + }, + }; +} + +const EIP712_DOMAIN_PRIMARY = 'EIP712Domain'; + +/** EIP-712 v4 digest buffer for the structured data (matches ETH-like `encodeTypedData` in this package). */ +export function encodeErc20VotesDelegationTypedDataDigest(typedData: Erc20VotesDelegationTypedData): Buffer { + const raw = JSON.parse(JSON.stringify(typedData)) as TypedMessage; + const sanitized = TypedDataUtils.sanitizeData(raw); + const parts: Buffer[] = [Buffer.from('1901', 'hex')]; + parts.push( + TypedDataUtils.hashStruct(EIP712_DOMAIN_PRIMARY, sanitized.domain, sanitized.types, SignTypedDataVersion.V4) + ); + if (sanitized.primaryType !== EIP712_DOMAIN_PRIMARY) { + parts.push( + TypedDataUtils.hashStruct( + sanitized.primaryType as string, + sanitized.message, + sanitized.types, + SignTypedDataVersion.V4 + ) + ); + } + return Buffer.concat(parts); +} + +/** Hex-encoded digest for BitGo typed-data tx request `messageEncoded` / `typedDataEncoded`. */ +export function encodeErc20VotesDelegationTypedDataDigestHex(typedData: Erc20VotesDelegationTypedData): string { + return encodeErc20VotesDelegationTypedDataDigest(typedData).toString('hex'); +} + +export interface DelegateBySigCallParams { + delegatee: string; + nonce: ethers.BigNumberish; + expiry: ethers.BigNumberish; + v: number; + r: string; + s: string; +} + +/** ABI-encoded calldata for `delegateBySig` (contract call `data` field). */ +export function encodeDelegateBySigCalldata(params: DelegateBySigCallParams): string { + const iface = new ethers.utils.Interface(delegateBySigAbi); + return iface.encodeFunctionData('delegateBySig', [ + ethers.utils.getAddress(params.delegatee), + ethers.BigNumber.from(params.nonce), + ethers.BigNumber.from(params.expiry), + params.v, + params.r, + params.s, + ]); +} + +/** WLFI ERC-20 proxy on Ethereum mainnet (verify `eip712Domain()` before signing). */ +export const WLFI_ETHEREUM_MAINNET_PROXY = '0xda5e1988097297dcdc1f90d4dfe7909e847cbef6'; + +/** + * EIP-712 domain as returned by WLFI `eip712Domain()` on Ethereum mainnet (Apr 2026). + * Re-read on-chain after upgrades; name/version/verifyingContract can change. + */ +export function wlfiEthereumMainnetDelegationDomain(): Erc20VotesDelegationDomain { + return { + name: 'World Liberty Financial', + version: '2', + chainId: 1, + verifyingContract: WLFI_ETHEREUM_MAINNET_PROXY, + }; +} diff --git a/modules/abstract-eth/src/lib/eip712/index.ts b/modules/abstract-eth/src/lib/eip712/index.ts new file mode 100644 index 0000000000..ae057aa314 --- /dev/null +++ b/modules/abstract-eth/src/lib/eip712/index.ts @@ -0,0 +1 @@ +export * from './erc20VotesDelegation'; diff --git a/modules/abstract-eth/src/lib/index.ts b/modules/abstract-eth/src/lib/index.ts index f685804a50..e5014960f1 100644 --- a/modules/abstract-eth/src/lib/index.ts +++ b/modules/abstract-eth/src/lib/index.ts @@ -10,6 +10,7 @@ export * from './types'; export * from './utils'; export * from './walletUtil'; export * from './messages'; +export * from './eip712'; // for Backwards Compatibility import * as Interface from './iface'; diff --git a/modules/abstract-eth/test/unit/eip712/erc20VotesDelegation.ts b/modules/abstract-eth/test/unit/eip712/erc20VotesDelegation.ts new file mode 100644 index 0000000000..027db7c0d2 --- /dev/null +++ b/modules/abstract-eth/test/unit/eip712/erc20VotesDelegation.ts @@ -0,0 +1,85 @@ +import 'should'; +import { SignTypedDataVersion, TypedDataUtils } from '@metamask/eth-sig-util'; +import { ethers } from 'ethers'; + +import { + buildErc20VotesDelegationTypedData, + encodeDelegateBySigCalldata, + encodeErc20VotesDelegationTypedDataDigestHex, + wlfiEthereumMainnetDelegationDomain, +} from '../../../src/lib/eip712/erc20VotesDelegation'; + +describe('ERC20Votes delegation EIP-712', function () { + it('buildErc20VotesDelegationTypedData matches OZ Delegation struct', function () { + const typedData = buildErc20VotesDelegationTypedData({ + domain: wlfiEthereumMainnetDelegationDomain(), + message: { + delegatee: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + nonce: 7, + expiry: 2000000000, + }, + }); + + typedData.primaryType.should.equal('Delegation'); + typedData.message.nonce.should.equal('7'); + typedData.message.expiry.should.equal('2000000000'); + typedData.message.delegatee.should.equal('0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'); + + const raw = JSON.parse(JSON.stringify(typedData)); + const sanitized = TypedDataUtils.sanitizeData(raw); + const domainHash = TypedDataUtils.hashStruct( + 'EIP712Domain', + sanitized.domain, + sanitized.types, + SignTypedDataVersion.V4 + ); + const structHash = TypedDataUtils.hashStruct( + 'Delegation', + sanitized.message, + sanitized.types, + SignTypedDataVersion.V4 + ); + domainHash.should.be.instanceOf(Buffer); + structHash.should.be.instanceOf(Buffer); + domainHash.length.should.equal(32); + structHash.length.should.equal(32); + }); + + it('encodeErc20VotesDelegationTypedDataDigestHex matches EIP-712 prefix + domain + Delegation', function () { + const typedData = buildErc20VotesDelegationTypedData({ + domain: wlfiEthereumMainnetDelegationDomain(), + message: { + delegatee: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + nonce: 7, + expiry: 2000000000, + }, + }); + const hex = encodeErc20VotesDelegationTypedDataDigestHex(typedData); + hex.should.match(/^[0-9a-f]+$/i); + (hex.length / 2).should.equal(66); // 0x1901 + 32-byte domain + 32-byte struct + encodeErc20VotesDelegationTypedDataDigestHex(typedData).should.equal(hex); + }); + + it('encodeDelegateBySigCalldata encodes OZ delegateBySig', function () { + const r = '0x' + '11'.repeat(32); + const s = '0x' + '22'.repeat(32); + const data = encodeDelegateBySigCalldata({ + delegatee: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + nonce: 1, + expiry: 2, + v: 28, + r, + s, + }); + const iface = new ethers.utils.Interface([ + 'function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)', + ]); + const decoded = iface.decodeFunctionData('delegateBySig', data); + decoded.delegatee.should.equal('0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'); + decoded.nonce.toString().should.equal('1'); + decoded.expiry.toString().should.equal('2'); + decoded.v.should.equal(28); + decoded.r.should.equal(r); + decoded.s.should.equal(s); + }); +}); diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index de7fd8bfdd..d6ee81be43 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -43,8 +43,8 @@ import { GShare, SignShare } from '../../../account-lib/mpc/tss'; import { RequestTracer } from '../util'; import { envRequiresBitgoPubGpgKeyConfig, getBitgoMpcGpgPubKey } from '../../tss/bitgoPubKeys'; import { getBitgoGpgPubKey } from '../opengpgUtils'; -import assert from 'assert'; import { MessageStandardType } from '../messageTypes'; +import assert from 'assert'; /** * BaseTssUtil class which different signature schemes have to extend @@ -433,6 +433,7 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil isTss: params.isTss, messageRaw: params.typedDataRaw, messageEncoded: params.typedDataEncoded ?? '', + messageStandardType: params.messageStandardType ?? MessageStandardType.EIP712, }; return this.createTxRequestBase(intentOptions, apiVersion, preview, params.reqId); diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts index ba2f3b6d46..8d42f314b8 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts @@ -222,6 +222,8 @@ export interface IntentOptionsForMessage extends IntentOptionsBase { export interface IntentOptionsForTypedData extends IntentOptionsBase { typedDataRaw: string; typedDataEncoded?: string; + /** Required for OVC / `verifyOffchainMessages` on custodial exports (e.g. TAT); defaults to EIP712 in `createTxRequestWithIntentForTypedDataSigning`. */ + messageStandardType?: MessageStandardType; } export interface PrebuildTransactionWithIntentOptions extends IntentOptionsBase { @@ -302,6 +304,7 @@ export interface PopulatedIntentForTypedDataSigning extends PopulatedIntentBase messageRaw: string; messageEncoded: string; custodianMessageId?: string; + messageStandardType?: MessageStandardType; } export interface PopulatedIntent extends PopulatedIntentBase { diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 4f6616d06f..d9617c1576 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -49,6 +49,7 @@ import { TokenType, TxRequest, } from '../utils'; +import { MessageStandardType } from '../utils/messageTypes'; import { postWithCodec } from '../utils/postWithCodec'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; import EddsaUtils from '../utils/tss/eddsa'; @@ -4092,6 +4093,7 @@ export class Wallet implements IWallet { isTss: true, typedDataRaw: params.typedData.typedDataRaw, typedDataEncoded: params.typedData.typedDataEncoded!.toString('hex'), + messageStandardType: MessageStandardType.EIP712, }; txRequest = await this.tssUtils!.createTxRequestWithIntentForTypedDataSigning(intentOptions); params.typedData.txRequestId = txRequest.txRequestId;