Wallet adapter
Write operations need a signature from your wallet. Nutrimatic does not ship a wallet product and does not manage connections, sessions, or account pickers.
You implement a small WalletAdapter and pass it in SDK config when your app runs writes (and optionally so portfolio reads can omit owner). The SDK asks for signatures via SignRequest; your adapter returns a SignedPayload. A multi-step plan may call sign more than once — Settlement — Signing across steps. Init: Configuration — Modes.
Nutrimatic prepares what must be signed; your adapter asks the user’s wallet (MetaMask, WalletConnect, custodial signer, …) to sign that payload and returns the result. You do not build swap calldata yourself.
What you implement vs what Nutrimatic does
| You | Nutrimatic SDK |
|---|---|
| Connect MetaMask, WalletConnect, a custodial signer, etc. | Request a typed SignRequest for the current operation |
| Show connect / account UI | Pass the signed result so Nutrimatic can execute |
| Map chain switches and user rejection to your UX | Surface stable error codes (e.g. USER_REJECTED) |
Config
import { createNutrimaticSdk } from '@nutrimatic/sdk'
const sdk = createNutrimaticSdk({
ledgers: [{ id: 'evm:421018', kind: 'evm', chainId: 421018, alias: 'infinite-mainnet' }],
mode: 'full',
integrationContext: 'client',
wallet: myWalletAdapter,
})wallet is optional at the type level; write methods that require signing fail if it is missing when the operation needs it. See Configuration.
Browser wallet (EVM) — what you wire
Nutrimatic does not ship MetaMask. You connect the wallet in your UI, then implement WalletAdapter so the SDK can ask for signatures.
Typical flow:
- Connect — your app asks the browser wallet for accounts (e.g. MetaMask
eth_requestAccounts). getAddress/getLedger— return the active account and the Nutrimatic ledger id for the chain the user selected (e.g.evm:421018for Infinite Mainnet).sign— when the SDK callssign(request), ifrequest.kindis'evm', forwardrequest.payloadto the wallet with the RPC your library uses for that payload shape (often typed-data or transaction signing). Return the signed material inSignedPayload.- If the user rejects in the wallet UI, surface that as
USER_REJECTED(or let your library’s rejection map to it).
// Shape only — use your wallet library’s APIs for connect + sign.
const browserWallet: WalletAdapter = {
async getAddress() {
return currentAccountAddress // from your connect flow
},
async getLedger() {
return 'evm:421018' // map the wallet’s chain to a Nutrimatic ledger id
},
async sign(request) {
if (request.kind !== 'evm') {
throw new Error(`Unsupported ledger kind: ${request.kind}`)
}
// If request.expiresAt is set (Unix ms), do not sign after that time
const signed = await walletLibrary.signNutrimaticEvmPayload(request.payload)
return { ledger: request.ledger, kind: request.kind, payload: signed }
},
}You still own connect buttons, account switching, and chain switching. The SDK only calls these three methods when a write (or a default owner) needs them.
WalletAdapter
Do not assume every request is an EVM transaction — ledger / kind tell you which network family the payload belongs to.
interface WalletAdapter {
getAddress(): Promise<string>
getLedger(): Promise<LedgerId>
sign(request: SignRequest): Promise<SignedPayload>
}| Method | Description |
|---|---|
getAddress() | Address / account the user is acting as on the current ledger family. |
getLedger() | Canonical ledger ID currently selected in the wallet (never an alias). |
sign(request) | Sign the opaque SignRequest.payload for that ledger family and return a SignedPayload. |
SignRequest
interface SignRequest {
readonly ledger: LedgerId
readonly kind: 'evm' | 'bitcoin' | 'cosmos' | string
readonly purpose:
| 'swap'
| 'liquidity_add'
| 'liquidity_remove'
| 'settlement'
| 'approve'
| 'stake'
| 'unstake'
| 'stake_claim'
| 'farm_deposit'
| 'farm_withdraw'
| 'farm_harvest'
| string
readonly payload: unknown
readonly expiresAt?: number
}| Field | Description |
|---|---|
ledger | Canonical ledger ID this signature is for. |
kind | Network family (evm, bitcoin, cosmos, …). Your adapter uses this to interpret payload. |
purpose | Why the SDK is asking (swap, approve, stake, farm_deposit, …). Useful for UI copy. |
payload | Opaque signing input for that family — sign what Nutrimatic sends; do not rebuild the operation yourself. |
expiresAt | Optional. Unix time in milliseconds after which you should not sign (treat as expired). |
Example (shape your adapter receives — payload stays opaque):
{
"ledger": "evm:421018",
"kind": "evm",
"purpose": "swap",
"payload": { "/* ledger-native signing input */": true },
"expiresAt": 1719799200000
}Treat payload as opaque unless your adapter knows how to handle that kind. Sign what the SDK provides; do not rebuild the operation yourself.
SignedPayload
interface SignedPayload {
readonly ledger: LedgerId
readonly kind: string
readonly payload: unknown
}Example (what your adapter returns):
{
"ledger": "evm:421018",
"kind": "evm",
"payload": { "/* signed material for that family */": true }
}Return the signed material in the form your ledger family expects. The SDK submits it for execution.
Minimal mock (tests / local)
const mockWallet: WalletAdapter = {
async getAddress() {
return '0x…'
},
async getLedger() {
return 'evm:421018'
},
async sign(request) {
return {
ledger: request.ledger,
kind: request.kind,
payload: { signed: true, request },
}
},
}Errors
| Code | When |
|---|---|
WALLET_REQUIRED | Write needs a wallet and none was configured |
USER_REJECTED | User (or adapter) declined to sign |
QUOTE_EXPIRED | Underlying quote/request expired before sign completed |
READ_ONLY_MODE | Init used read-only mode — Modes |