Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/advanced/network-implementation-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,14 @@ The signer interface provides a consistent API across different networks:

```typescript
// Universal signer interface
interface IUniSigner<TAccount, TSignArgs, TBroadcastOpts, TBroadcastResponse> {
interface IUniSigner<
TAccount,
TSignArgs,
TBroadcastOpts,
TBroadcastResponse,
TQueryClient extends IQueryClient = IQueryClient
> {
queryClient: TQueryClient;
// Account management
getAccounts(): Promise<readonly TAccount[]>;

Expand All @@ -476,10 +483,10 @@ interface ISigned<TBroadcastOpts, TBroadcastResponse> {
}

// Network-specific signer implementation
class NetworkSigner implements IUniSigner<NetworkAccount, NetworkSignArgs, NetworkBroadcastOpts, NetworkBroadcastResponse> {
class NetworkSigner implements IUniSigner<NetworkAccount, NetworkSignArgs, NetworkBroadcastOpts, NetworkBroadcastResponse, IQueryClient> {
constructor(
private wallet: IWallet,
private queryClient: IQueryClient,
public readonly queryClient: IQueryClient,
private config: NetworkSignerConfig
) {}

Expand Down
3 changes: 3 additions & 0 deletions docs/advanced/signer.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ export interface IUniSigner<
TSignArgs = unknown,
TBroadcastOpts = unknown,
TBroadcastResponse extends IBroadcastResult<TTxResp> = IBroadcastResult<TTxResp>,
TQueryClient extends IQueryClient = IQueryClient,
> {
// Query interface used for chain reads and broadcasting
queryClient: TQueryClient;
// Account management
getAccounts(): Promise<readonly TAccount[]>;

Expand Down
3 changes: 3 additions & 0 deletions docs/advanced/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ interface IUniSigner<
TSignArgs = unknown,
TBroadcastOpts = unknown,
TBroadcastResponse extends IBroadcastResult<TTxResp> = IBroadcastResult<TTxResp>,
TQueryClient extends IQueryClient = IQueryClient,
> {
// Query interface used for chain reads and broadcasting
queryClient: TQueryClient;
// Account management
getAccounts(): Promise<readonly TAccount[]>;

Expand Down
3 changes: 2 additions & 1 deletion docs/networks/_meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cosmos": "Cosmos",
"ethereum": "Ethereum",
"injective": "Injective"
"injective": "Injective",
"solana": "Solana"
}
71 changes: 70 additions & 1 deletion docs/networks/ethereum/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<bigint>({
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
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions docs/networks/solana/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"index": "Overview"
}
1 change: 1 addition & 0 deletions docs/networks/solana/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Solana Chain
2 changes: 2 additions & 0 deletions docs/packages/types/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ interface IUniSigner<
TSignArgs = unknown,
TBroadcastOpts = unknown,
TBroadcastResponse extends IBroadcastResult<TTxResp> = IBroadcastResult<TTxResp>,
TQueryClient extends IQueryClient = IQueryClient,
> {
queryClient: TQueryClient;
getAccounts(): Promise<readonly TAccount[]>;
signArbitrary(data: Uint8Array, index?: number): Promise<ICryptoBytes>;
sign(args: TSignArgs): Promise<ISigned<TBroadcastOpts, TBroadcastResponse>>;
Expand Down
6 changes: 5 additions & 1 deletion networks/cosmos/src/signers/base-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<readonly AccountData[]> {
if (isOfflineAminoSigner(this.auth) || isOfflineDirectSigner(this.auth)) {
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions networks/cosmos/src/signers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>;
getChainId(): Promise<string>;
Expand Down Expand Up @@ -268,4 +269,4 @@ export type TxOptions = {
// Document types
export type CosmosDirectDoc = SignDoc;
export type CosmosAminoDoc = StdSignDoc;
export type CosmosTx = TxRaw;
export type CosmosTx = TxRaw;
20 changes: 12 additions & 8 deletions networks/cosmos/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface Account {
readonly sequence: number;
}

export interface AccountFromAnyOption {
readonly pubkeyDecoders?: Record<string, (pubkey: Any) => Pubkey>;
}

/**
* Extracts a BaseAccount from simple wrapper account types using binary parsing.
* This handles simple wrapper account types like ModuleAccount that contain
Expand Down Expand Up @@ -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),
};
Expand All @@ -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),
};
Expand All @@ -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),
};
Expand All @@ -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),
};
Expand All @@ -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),
};
Expand All @@ -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),
};
Expand Down Expand Up @@ -235,4 +240,3 @@ export { generateMnemonic };

// Re-export fee helpers
export * from './fee';

38 changes: 37 additions & 1 deletion networks/cosmos/src/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -295,4 +332,3 @@ describe("accountFromAny", () => {
});
});
});

7 changes: 5 additions & 2 deletions networks/cosmos/src/wallets/secp256k1hd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down Expand Up @@ -200,4 +203,4 @@ export class Secp256k1HDWallet extends BaseWallet implements IWallet {
signAmino: async (signerAddress: string, signDoc: StdSignDoc) => this.signAmino(signerAddress, signDoc)
}
}
}
}
4 changes: 4 additions & 0 deletions networks/ethereum/src/signers/base-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<readonly EthereumAccountData[]> {
const accounts = await this.auth.getAccounts();
Expand Down
3 changes: 2 additions & 1 deletion networks/ethereum/src/signers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ export interface IEthereumSigner extends IUniSigner<
EthereumAccountData,
EthereumSignArgs,
EthereumBroadcastOptions,
EthereumBroadcastResponse
EthereumBroadcastResponse,
IEthereumQueryClient
> {
/** Get Ethereum addresses */
getAddresses(): Promise<string[]>;
Expand Down
15 changes: 13 additions & 2 deletions packages/pubkey/src/pubkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (pubkey: Any) => 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`);
}
Expand Down
Loading
Loading