From 3ae82ef21bbce70cbcb610feefe50975c2c4e992 Mon Sep 17 00:00:00 2001 From: Zhi Zhen Date: Thu, 2 Oct 2025 09:28:47 +0800 Subject: [PATCH 1/2] add queryClient to unisigner --- docs/advanced/network-implementation-guide.md | 13 +++- docs/advanced/signer.md | 3 + docs/advanced/tutorial.md | 3 + docs/networks/_meta.json | 3 +- docs/networks/ethereum/index.mdx | 71 ++++++++++++++++++- docs/networks/solana/_meta.json | 3 + docs/networks/solana/index.mdx | 1 + docs/packages/types/index.mdx | 2 + networks/cosmos/src/signers/base-signer.ts | 6 +- networks/cosmos/src/signers/types.ts | 5 +- networks/cosmos/src/wallets/secp256k1hd.ts | 7 +- networks/ethereum/src/signers/base-signer.ts | 4 ++ networks/ethereum/src/signers/types.ts | 3 +- packages/types/README.md | 2 + packages/types/src/signer.ts | 7 +- 15 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 docs/networks/solana/_meta.json create mode 100644 docs/networks/solana/index.mdx diff --git a/docs/advanced/network-implementation-guide.md b/docs/advanced/network-implementation-guide.md index 558d8ab80..7526635fb 100644 --- a/docs/advanced/network-implementation-guide.md +++ b/docs/advanced/network-implementation-guide.md @@ -456,7 +456,14 @@ The signer interface provides a consistent API across different networks: ```typescript // Universal signer interface -interface IUniSigner { +interface IUniSigner< + TAccount, + TSignArgs, + TBroadcastOpts, + TBroadcastResponse, + TQueryClient extends IQueryClient = IQueryClient +> { + queryClient: TQueryClient; // Account management getAccounts(): Promise; @@ -476,10 +483,10 @@ interface ISigned { } // Network-specific signer implementation -class NetworkSigner implements IUniSigner { +class NetworkSigner implements IUniSigner { constructor( private wallet: IWallet, - private queryClient: IQueryClient, + public readonly queryClient: IQueryClient, private config: NetworkSignerConfig ) {} diff --git a/docs/advanced/signer.md b/docs/advanced/signer.md index cc82b7f0d..139457abc 100644 --- a/docs/advanced/signer.md +++ b/docs/advanced/signer.md @@ -161,7 +161,10 @@ export interface IUniSigner< TSignArgs = unknown, TBroadcastOpts = unknown, TBroadcastResponse extends IBroadcastResult = IBroadcastResult, + TQueryClient extends IQueryClient = IQueryClient, > { + // Query interface used for chain reads and broadcasting + queryClient: TQueryClient; // Account management getAccounts(): Promise; diff --git a/docs/advanced/tutorial.md b/docs/advanced/tutorial.md index 31b894663..6b865a64f 100644 --- a/docs/advanced/tutorial.md +++ b/docs/advanced/tutorial.md @@ -57,7 +57,10 @@ interface IUniSigner< TSignArgs = unknown, TBroadcastOpts = unknown, TBroadcastResponse extends IBroadcastResult = IBroadcastResult, + TQueryClient extends IQueryClient = IQueryClient, > { + // Query interface used for chain reads and broadcasting + queryClient: TQueryClient; // Account management getAccounts(): Promise; diff --git a/docs/networks/_meta.json b/docs/networks/_meta.json index b87d48ae8..e87daf52e 100644 --- a/docs/networks/_meta.json +++ b/docs/networks/_meta.json @@ -1,5 +1,6 @@ { "cosmos": "Cosmos", "ethereum": "Ethereum", - "injective": "Injective" + "injective": "Injective", + "solana": "Solana" } \ No newline at end of file diff --git a/docs/networks/ethereum/index.mdx b/docs/networks/ethereum/index.mdx index a77093b37..c6440b340 100644 --- a/docs/networks/ethereum/index.mdx +++ b/docs/networks/ethereum/index.mdx @@ -230,6 +230,75 @@ console.log("Endpoint:", queryClient.endpoint); await queryClient.disconnect(); ``` +### Frontend: window.ethereum (Browser Wallet) + +Use the injected EIP-1193 provider (e.g., MetaMask) to send ETH and execute contract functions by ABI. + +```typescript +import { SignerFromBrowser } from "@interchainjs/ethereum"; + +// Initialize with injected provider +const signer = new SignerFromBrowser(window.ethereum); + +// Request accounts (prompts the wallet) +const [address] = await signer.getAddresses(true); +console.log("Connected address:", address); + +// 1) Send ETH +const { transactionHash, wait } = await signer.send({ + to: "0xRecipient...", + // value accepts bigint | number | string; converted to 0x hex automatically + value: 1000000000000000n // 0.001 ETH +}); +console.log("ETH tx:", transactionHash); +const receipt = await wait(); +console.log("ETH receipt:", receipt.status); + +// 2) Read contract function (by ABI) +// Example: ERC20 balanceOf(address) +const erc20Abi = [ + { + name: "balanceOf", + type: "function", + inputs: [{ name: "owner", type: "address" }], + outputs: [{ name: "balance", type: "uint256" }] + } +]; +const tokenAddress = "0xToken..."; +const balance = await signer.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "balanceOf", + params: [address] +}); +console.log("Token balance:", balance.toString()); + +// 3) Write contract function (by ABI) +// Example: ERC20 transfer(address,uint256) returns (bool) +const transferAbi = [ + { + name: "transfer", + type: "function", + inputs: [ + { name: "to", type: "address" }, + { name: "amount", type: "uint256" } + ], + outputs: [{ name: "ok", type: "bool" }] + } +]; +const { transactionHash: txHash2, wait: wait2 } = await signer.writeContract({ + address: tokenAddress, + abi: transferAbi, + functionName: "transfer", + params: ["0xRecipient...", 1000000000000000000n] // 1 token (18 decimals) + // Optional gas options: + // gas, gasPrice, maxFeePerGas, maxPriorityFeePerGas, nonce, chainId +}); +console.log("transfer tx:", txHash2); +const transferReceipt = await wait2(); +console.log("transfer status:", transferReceipt.status); +``` + ### Using Ethereum Signers #### Creating Signers @@ -466,7 +535,7 @@ console.log(toChecksumAddress(lower)); ### Legacy Support - **SignerFromPrivateKey**: Original implementation (maintained for backward compatibility) -- **SignerFromBrowser**: Browser wallet integration (maintained for backward compatibility) +- **SignerFromBrowser**: Browser wallet integration via `window.ethereum` (EIP-1193) ## For Developers diff --git a/docs/networks/solana/_meta.json b/docs/networks/solana/_meta.json new file mode 100644 index 000000000..356de82b4 --- /dev/null +++ b/docs/networks/solana/_meta.json @@ -0,0 +1,3 @@ +{ + "index": "Overview" +} \ No newline at end of file diff --git a/docs/networks/solana/index.mdx b/docs/networks/solana/index.mdx new file mode 100644 index 000000000..1193cf265 --- /dev/null +++ b/docs/networks/solana/index.mdx @@ -0,0 +1 @@ +Solana Chain \ No newline at end of file diff --git a/docs/packages/types/index.mdx b/docs/packages/types/index.mdx index 0844a0437..86cdfe618 100644 --- a/docs/packages/types/index.mdx +++ b/docs/packages/types/index.mdx @@ -55,7 +55,9 @@ interface IUniSigner< TSignArgs = unknown, TBroadcastOpts = unknown, TBroadcastResponse extends IBroadcastResult = IBroadcastResult, + TQueryClient extends IQueryClient = IQueryClient, > { + queryClient: TQueryClient; getAccounts(): Promise; signArbitrary(data: Uint8Array, index?: number): Promise; sign(args: TSignArgs): Promise>; diff --git a/networks/cosmos/src/signers/base-signer.ts b/networks/cosmos/src/signers/base-signer.ts index e07a2094a..d9d82e3cc 100644 --- a/networks/cosmos/src/signers/base-signer.ts +++ b/networks/cosmos/src/signers/base-signer.ts @@ -43,6 +43,10 @@ export abstract class BaseCosmosSigner implements ICosmosSigner, ISigningClient this.config.queryClient = originalQueryClient; } + get queryClient() { + return this.config.queryClient; + } + // ICosmosSigner interface methods async getAccounts(): Promise { if (isOfflineAminoSigner(this.auth) || isOfflineDirectSigner(this.auth)) { @@ -319,7 +323,7 @@ export abstract class BaseCosmosSigner implements ICosmosSigner, ISigningClient const tx = Tx.fromPartial({ body: txBody, authInfo: authInfo, - signatures: new Uint8Array([0]), // Empty signatures for simulation + signatures: [new Uint8Array(0)], // Empty signatures for simulation }); // Encode transaction to bytes diff --git a/networks/cosmos/src/signers/types.ts b/networks/cosmos/src/signers/types.ts index d8708be75..5585368ac 100644 --- a/networks/cosmos/src/signers/types.ts +++ b/networks/cosmos/src/signers/types.ts @@ -155,7 +155,8 @@ export interface ICosmosSigner extends IUniSigner< AccountData, // account type CosmosSignArgs, // sign args CosmosBroadcastOptions, // broadcast options - CosmosBroadcastResponse // broadcast response + CosmosBroadcastResponse, // broadcast response + ICosmosQueryClient // query client > { getAddresses(): Promise; getChainId(): Promise; @@ -268,4 +269,4 @@ export type TxOptions = { // Document types export type CosmosDirectDoc = SignDoc; export type CosmosAminoDoc = StdSignDoc; -export type CosmosTx = TxRaw; \ No newline at end of file +export type CosmosTx = TxRaw; diff --git a/networks/cosmos/src/wallets/secp256k1hd.ts b/networks/cosmos/src/wallets/secp256k1hd.ts index d12405d9e..42004a3e9 100644 --- a/networks/cosmos/src/wallets/secp256k1hd.ts +++ b/networks/cosmos/src/wallets/secp256k1hd.ts @@ -80,9 +80,12 @@ export class Secp256k1HDWallet extends BaseWallet implements IWallet { const formatFn = resolveSignatureFormat('compact'); const compactSignature = formatFn ? formatFn(signatureResult.value) : signatureResult.value; + // Create the signature object with pub_key and signature fields + const signature = encodeSecp256k1Signature(publicKey.value.value, compactSignature); + return { signed: signDoc, - signature: compactSignature + signature }; } @@ -200,4 +203,4 @@ export class Secp256k1HDWallet extends BaseWallet implements IWallet { signAmino: async (signerAddress: string, signDoc: StdSignDoc) => this.signAmino(signerAddress, signDoc) } } -} \ No newline at end of file +} diff --git a/networks/ethereum/src/signers/base-signer.ts b/networks/ethereum/src/signers/base-signer.ts index 7bd54b749..fe6686855 100644 --- a/networks/ethereum/src/signers/base-signer.ts +++ b/networks/ethereum/src/signers/base-signer.ts @@ -32,6 +32,10 @@ export abstract class BaseEthereumSigner implements IEthereumSigner { this.config.queryClient = originalQueryClient; } + get queryClient() { + return this.config.queryClient; + } + // IUniSigner interface methods async getAccounts(): Promise { const accounts = await this.auth.getAccounts(); diff --git a/networks/ethereum/src/signers/types.ts b/networks/ethereum/src/signers/types.ts index 8f984ab9d..faa14b798 100644 --- a/networks/ethereum/src/signers/types.ts +++ b/networks/ethereum/src/signers/types.ts @@ -149,7 +149,8 @@ export interface IEthereumSigner extends IUniSigner< EthereumAccountData, EthereumSignArgs, EthereumBroadcastOptions, - EthereumBroadcastResponse + EthereumBroadcastResponse, + IEthereumQueryClient > { /** Get Ethereum addresses */ getAddresses(): Promise; diff --git a/packages/types/README.md b/packages/types/README.md index 0844a0437..86cdfe618 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -55,7 +55,9 @@ interface IUniSigner< TSignArgs = unknown, TBroadcastOpts = unknown, TBroadcastResponse extends IBroadcastResult = IBroadcastResult, + TQueryClient extends IQueryClient = IQueryClient, > { + queryClient: TQueryClient; getAccounts(): Promise; signArbitrary(data: Uint8Array, index?: number): Promise; sign(args: TSignArgs): Promise>; diff --git a/packages/types/src/signer.ts b/packages/types/src/signer.ts index 0edc1ba61..fce3901ba 100644 --- a/packages/types/src/signer.ts +++ b/packages/types/src/signer.ts @@ -1,5 +1,6 @@ import Decimal from 'decimal.js'; import { ICryptoBytes, IAccount } from './auth'; +import { IQueryClient } from './query'; import { sign } from 'crypto'; /** @@ -38,7 +39,11 @@ export interface IUniSigner< TSignArgs = unknown, TBroadcastOpts = unknown, TBroadcastResponse extends IBroadcastResult = IBroadcastResult, + TQueryClient extends IQueryClient = IQueryClient, > { + // Query interface used for chain reads and broadcasting + queryClient: TQueryClient; + // Account management getAccounts(): Promise; @@ -85,4 +90,4 @@ export interface Attribute { export interface Event { type: string; attributes: readonly Attribute[]; -} \ No newline at end of file +} From 9ffd0355186de1ba71445e12e4f051865382d01e Mon Sep 17 00:00:00 2001 From: Zhi Zhen Date: Thu, 2 Oct 2025 12:05:09 +0800 Subject: [PATCH 2/2] add pubkey decoder logic to accountFromAny --- networks/cosmos/src/utils/index.ts | 20 +++++++------ networks/cosmos/src/utils/utils.test.ts | 38 ++++++++++++++++++++++++- packages/pubkey/src/pubkey.ts | 15 ++++++++-- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/networks/cosmos/src/utils/index.ts b/networks/cosmos/src/utils/index.ts index 664fbf1c6..5b57d5319 100644 --- a/networks/cosmos/src/utils/index.ts +++ b/networks/cosmos/src/utils/index.ts @@ -32,6 +32,10 @@ export interface Account { readonly sequence: number; } +export interface AccountFromAnyOption { + readonly pubkeyDecoders?: Record Pubkey>; +} + /** * Extracts a BaseAccount from simple wrapper account types using binary parsing. * This handles simple wrapper account types like ModuleAccount that contain @@ -86,13 +90,14 @@ function extractBaseAccountFromWrapper(value: Uint8Array): BaseAccount | null { * @returns A standardized Account object * @throws Error if the account type is not supported */ -export function accountFromAny(accountAny: Any): Account { +export function accountFromAny(accountAny: Any, opts?: AccountFromAnyOption): Account { + const pubkeyDecoders = opts?.pubkeyDecoders; switch (accountAny.typeUrl) { case "/cosmos.auth.v1beta1.BaseAccount": { const baseAccount = BaseAccount.decode(accountAny.value); return { address: baseAccount.address, - pubkey: decodeOptionalPubkey(baseAccount.pubKey), + pubkey: decodeOptionalPubkey(baseAccount.pubKey, pubkeyDecoders), accountNumber: Number(baseAccount.accountNumber), sequence: Number(baseAccount.sequence), }; @@ -107,7 +112,7 @@ export function accountFromAny(accountAny: Any): Account { return { address: baseAccount.address, - pubkey: decodeOptionalPubkey(baseAccount.pubKey), + pubkey: decodeOptionalPubkey(baseAccount.pubKey, pubkeyDecoders), accountNumber: Number(baseAccount.accountNumber), sequence: Number(baseAccount.sequence), }; @@ -122,7 +127,7 @@ export function accountFromAny(accountAny: Any): Account { return { address: baseAccount.address, - pubkey: decodeOptionalPubkey(baseAccount.pubKey), + pubkey: decodeOptionalPubkey(baseAccount.pubKey, pubkeyDecoders), accountNumber: Number(baseAccount.accountNumber), sequence: Number(baseAccount.sequence), }; @@ -137,7 +142,7 @@ export function accountFromAny(accountAny: Any): Account { return { address: baseAccount.address, - pubkey: decodeOptionalPubkey(baseAccount.pubKey), + pubkey: decodeOptionalPubkey(baseAccount.pubKey, pubkeyDecoders), accountNumber: Number(baseAccount.accountNumber), sequence: Number(baseAccount.sequence), }; @@ -152,7 +157,7 @@ export function accountFromAny(accountAny: Any): Account { return { address: baseAccount.address, - pubkey: decodeOptionalPubkey(baseAccount.pubKey), + pubkey: decodeOptionalPubkey(baseAccount.pubKey, pubkeyDecoders), accountNumber: Number(baseAccount.accountNumber), sequence: Number(baseAccount.sequence), }; @@ -164,7 +169,7 @@ export function accountFromAny(accountAny: Any): Account { if (baseAccount) { return { address: baseAccount.address, - pubkey: decodeOptionalPubkey(baseAccount.pubKey), + pubkey: decodeOptionalPubkey(baseAccount.pubKey, pubkeyDecoders), accountNumber: Number(baseAccount.accountNumber), sequence: Number(baseAccount.sequence), }; @@ -235,4 +240,3 @@ export { generateMnemonic }; // Re-export fee helpers export * from './fee'; - diff --git a/networks/cosmos/src/utils/utils.test.ts b/networks/cosmos/src/utils/utils.test.ts index d51ce0ce1..9cafe208e 100644 --- a/networks/cosmos/src/utils/utils.test.ts +++ b/networks/cosmos/src/utils/utils.test.ts @@ -71,6 +71,43 @@ describe("accountFromAny", () => { sequence: 7, }); }); + + it("should use custom pubkey decoder when decodePubkey fails", () => { + const customTypeUrl = "/custom.crypto.v1.CustomPubKey"; + const fallbackPubkey = testPubkey; + + const baseAccount: BaseAccount = { + address: testAddress, + pubKey: { + typeUrl: customTypeUrl, + value: Uint8Array.from([1, 2, 3]), + }, + accountNumber: testAccountNumber, + sequence: testSequence, + }; + + const accountAny: Any = { + typeUrl: "/cosmos.auth.v1beta1.BaseAccount", + value: BaseAccount.encode(baseAccount).finish(), + }; + + const decoder = jest.fn().mockReturnValue(fallbackPubkey); + + const result = accountFromAny(accountAny, { + pubkeyDecoders: { + [customTypeUrl]: decoder, + }, + }); + + expect(decoder).toHaveBeenCalledTimes(1); + expect(decoder).toHaveBeenCalledWith(expect.objectContaining({ typeUrl: customTypeUrl })); + expect(result).toEqual({ + address: testAddress, + pubkey: fallbackPubkey, + accountNumber: 42, + sequence: 7, + }); + }); }); describe("Wrapper account types using binary parsing", () => { @@ -295,4 +332,3 @@ describe("accountFromAny", () => { }); }); }); - diff --git a/packages/pubkey/src/pubkey.ts b/packages/pubkey/src/pubkey.ts index e76a014ed..9fa2e36bd 100644 --- a/packages/pubkey/src/pubkey.ts +++ b/packages/pubkey/src/pubkey.ts @@ -108,12 +108,23 @@ export function decodePubkey(pubkey: Any): Pubkey { * This supports single pubkeys such as Cosmos ed25519 and secp256k1 keys * as well as multisig threshold pubkeys. */ -export function decodeOptionalPubkey(pubkey: Any | null | undefined): Pubkey | null { +export function decodeOptionalPubkey( + pubkey: Any | null | undefined, + pubkeyDecoders?: Record Pubkey> +): Pubkey | null { if (!pubkey) return null; if (pubkey.typeUrl) { if (pubkey.value.length) { // both set - return decodePubkey(pubkey); + try { + return decodePubkey(pubkey); + } catch (error) { + const decoder = pubkeyDecoders?.[pubkey.typeUrl]; + if (decoder) { + return decoder(pubkey); + } + throw error; + } } else { throw new Error(`Pubkey is an Any with type URL '${pubkey.typeUrl}' but an empty value`); }