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
4 changes: 3 additions & 1 deletion docs/sdk/backend/device-wallet-factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ Returns `Promise<Hash>`.

## postCreateAccount {#postcreateaccount}

Registers a device wallet with the factory after it deploys. Use it right after [`createAccountWithEOA`](../mobile/device-wallet-factory.md#createaccountwitheoa) on the mobile surface, because a wallet the app deployed is not registered until this runs.
Registers a device wallet with the factory after it deploys. Use it after the app deploys the wallet itself, either with its first sponsored user operation or with [`createAccountWithEOA`](../mobile/device-wallet-factory.md#createaccountwitheoa), because a wallet the app deployed is not registered until this runs. Until then it cannot deploy eSIM wallets.

On a hosted RPC, wait a few confirmations before telling the app to go ahead. A bundler that simulates the app's next operation against a node one block behind still sees the wallet as unregistered and rejects it.

The salt has to match the one the deploy used. The factory recomputes the wallet's address from it to check the wallet is real.

Expand Down
2 changes: 1 addition & 1 deletion docs/sdk/backend/lazy-wallet-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const deployment = await admin.lazyWalletRegistry.deployLazyWalletAndSetESIMIden
);
```

Some errors here are not exported by name, `BatchSizeOutOfRangeError` among them. Catch them with `instanceof KokioError` and read `.code`.
The errors these calls throw are exported by name from both entry points: `BatchSizeOutOfRangeError`, `DepositOnResumeError`, `ESIMWalletNotLazyDeployedError`, `MissingBatchEventError` and `StalledBatchError`. All extend `KokioError`.

## batchPopulateHistory {#batchpopulatehistory}

Expand Down
6 changes: 3 additions & 3 deletions docs/sdk/backend/payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const usdcSymbol = stringToHex("USDC", { size: 32 });
// 0x5553444300000000000000000000000000000000000000000000000000000000
```

The USDC registered on the current deployment is a test token, not Circle's. Its address is on the [deployed addresses](../../contracts/deployments.md) page, alongside the second registered asset, `USD`, which has no token and stands for a card or bank payment.
`USDC` on the current deployment is Circle's Base Sepolia USDC. `USD` has no token and stands for a card or bank payment. Read any asset's token address with [`resolveAsset`](#resolveasset) rather than hardcoding it.

## registerAsset {#registerasset}

Expand Down Expand Up @@ -124,10 +124,10 @@ Returns `Promise<Address>`.

## settlementToken {#settlementtoken}

Reads the ERC-20 registered under the `USDC` symbol at configure time.
Reads the token set when the adapter was initialized, the one the vault is meant to end up holding. A purchase does not pay in it: it pays in the token of the asset it names, which `resolveAsset` returns.

```ts
const usdc = await admin.paymentAdapter.settlementToken();
const token = await admin.paymentAdapter.settlementToken();
```

Returns `Promise<Address>`.
Expand Down
2 changes: 2 additions & 0 deletions docs/sdk/backend/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const admin = new KokioAdmin(walletClient);

Give `http()` a real RPC URL. The SDK reads it to build the public client it uses for reads.

Writes sign locally with the key and go out as raw transactions, so any hosted RPC works and never sees the key. A connected wallet account works too.

## Which surfaces are ready when {#surfaces}

`deviceWalletFactory`, `eSIMWalletFactory`, `registry`, `lazyWalletRegistry`, `paymentAdapter` and `protocolAdmin` work as soon as the instance exists, because they are chain-wide. The two instance-scoped surfaces, `deviceWallet` and `eSIMWallet`, stay `undefined` until bound to an address.
Expand Down
20 changes: 18 additions & 2 deletions docs/sdk/mobile/device-wallet.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Device wallet (mobile)
description: kokio.deviceWallet wraps one user's Kokio device wallet, the ERC-4337 smart account owned by a passkey, with methods to attach eSIM wallets, grant fund access, transfer ownership and manage the gas deposit.
description: kokio.deviceWallet wraps one user's Kokio device wallet, the ERC-4337 smart account owned by a passkey, with methods to deploy and attach eSIM wallets, grant fund access, transfer ownership and manage the gas deposit.
---

# Device wallet (mobile) {#device-wallet-mobile}
Expand All @@ -17,7 +17,7 @@ const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash });
if (!receipt.success) throw new Error("operation reverted");
```

An operation whose calls revert is still mined and still returns a receipt, so check `success` rather than treating a resolved await as confirmation.
A call the contract would refuse throws `ContractRevertError` before it is sent, with the error name in `err.decoded?.errorName`. If the chain changes between that check and inclusion, the operation can still revert onchain. It is then mined and returns a receipt, so check `success` rather than treating a resolved await as confirmation.

## sendUserOperation {#senduseroperation}

Expand All @@ -31,6 +31,22 @@ const hash = await kokio.deviceWallet!.sendUserOperation([

Returns `Promise<Hash>`, the user operation hash.

## deployAndBindESIMWallet {#deployandbindesimwallet}

Deploys a new eSIM wallet for this device wallet and adds it to the device wallet's list, in one user operation and one passkey prompt. Use it when a user adds a new eSIM. Pass `grantAccessToFunds: true` to also let the new eSIM wallet pull tokens from this device wallet, which [`buyDataBundleWithToken`](./esim-wallet.md#buydatabundlewithtoken) needs when the eSIM wallet holds less than the price.

The device wallet has to be registered first, by the backend's [`postCreateAccount`](../backend/device-wallet-factory.md#postcreateaccount) or by a backend deploy, or the operation reverts. Each salt gives one address, so use a new salt for each eSIM wallet on the same device.

```ts
const { userOpHash, eSIMWalletAddress } = await kokio.deviceWallet!.deployAndBindESIMWallet(
1n, // salt
{ grantAccessToFunds: true },
);
kokio.setESIMWalletAddress(eSIMWalletAddress);
```

Returns `Promise<{ userOpHash: Hash; eSIMWalletAddress: Address }>`. The address is worked out before the operation lands, so wait for the receipt before reading from it.

## addESIMWallet {#addesimwallet}

Adds an eSIM wallet this device already owns onto the device wallet's list. Use it after an eSIM wallet transfer lands, once the eSIM wallet's `owner()` already points at this device wallet.
Expand Down
36 changes: 33 additions & 3 deletions docs/sdk/mobile/esim-wallet.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: eSIM wallet (mobile)
description: kokio.eSIMWallet buys data bundles for one Kokio eSIM, sets its price cap and moves it between devices, and kokio.eSIMWalletFactory deploys a new one.
description: kokio.eSIMWallet buys data bundles for one Kokio eSIM, with or without access to the device wallet's funds, sets its price cap and moves it between devices, and kokio.eSIMWalletFactory predicts a new one's address.
---

# eSIM wallet (mobile) {#esim-wallet-mobile}
Expand All @@ -26,6 +26,8 @@ Prices are whole US cents as a `bigint`, and a currency is named by a `bytes32`

Buys a data bundle for this eSIM, paid for in an ERC-20 the payment adapter accepts, which is USDC on Base Sepolia today. This is the everyday purchase flow.

If this eSIM wallet holds less than the price, the contract pulls the rest from the device wallet. That needs fund access, granted by [`deviceWallet.toggleAccessToFunds`](./device-wallet.md#toggleaccesstofunds) or by `deployAndBindESIMWallet` with `grantAccessToFunds: true`. Without it the purchase reverts with `FundsAccessRevoked`, and [`buyDataBundleWithTransfer`](#buydatabundlewithtransfer) is the one to use.

Check `priceCapUSDCents()` first, because a price above the cap reverts. Read [`kokio.paymentAdapter.quote(asset, priceUSDCents)`](./payments.md#quote) to size `maxAmountIn`, the most of `asset` this purchase may spend, in its smallest unit. Nothing moves the price between the quote and the purchase today, so passing that value straight through is enough. A swap path may show up later, which is why the contract takes a maximum rather than an exact amount.

`paymentReference` ties the purchase to its offchain order and is spendable once per eSIM wallet. The backend hands this to the app. The SDK never invents one.
Expand All @@ -45,6 +47,20 @@ const hash = await kokio.eSIMWallet!.buyDataBundleWithToken(

Returns `Promise<Hash>`, a user operation hash.

## buyDataBundleWithTransfer {#buydatabundlewithtransfer}

The same purchase as `buyDataBundleWithToken`, for an eSIM wallet with no access to the device wallet's funds. In one user operation the device wallet sends the eSIM wallet whatever it is short of the quote, then the purchase runs. The arguments are the same.

The device wallet has to hold enough of the asset's token. Only the shortfall is sent, so tokens already on the eSIM wallet are spent first.

```ts
const hash = await kokio.eSIMWallet!.buyDataBundleWithTransfer(
dataBundleDetails, asset, maxAmountIn, paymentReference,
);
```

Returns `Promise<Hash>`, a user operation hash.

## sendTokenToDeviceWallet {#sendtokentodevicewallet}

Sends an ERC-20 held by this eSIM wallet back to its owning device wallet. Nothing else moves a stray token balance off this wallet, so use this when one is stuck here after a handover or a refund.
Expand Down Expand Up @@ -143,11 +159,25 @@ Returns `Promise<DataBundleDetails>`, shaped `{ id, priceUSDCents, settlement }`

`kokio.eSIMWalletFactory`

Deploys a new eSIM wallet for a device wallet. Present as soon as `Kokio` has a `smartAccountClient`, chain-wide like the [device wallet factory](./device-wallet-factory.md). The contract is documented at [eSIM wallet factory](../../contracts/esim-wallet-factory.md).
Predicts and deploys eSIM wallets for a device wallet. Present as soon as `Kokio` has a `smartAccountClient`, chain-wide like the [device wallet factory](./device-wallet-factory.md). The contract is documented at [eSIM wallet factory](../../contracts/esim-wallet-factory.md).

To deploy a new eSIM wallet, use [`deviceWallet.deployAndBindESIMWallet`](./device-wallet.md#deployandbindesimwallet). It deploys and binds in one user operation and returns the new address.

### getCounterFactualAddress {#getcounterfactualaddress}

Works out the address an eSIM wallet will have for a device wallet and salt, before it is deployed. A plain read, no user operation.

```ts
const eSIMWalletAddress = await kokio.eSIMWalletFactory!.getCounterFactualAddress(deviceWalletAddress, 1n);
```

Returns `Promise<Address>`.

### deployESIMWalletWithUserOp {#deployesimwalletwithuserop}

Deploys a new eSIM wallet, owned by the given device wallet. Use it when a user is adding a new eSIM to a device wallet they already have. The device wallet sending the user operation has to be one the registry recognizes.
Deprecated. Use `deviceWallet.deployAndBindESIMWallet` instead. This deploys the eSIM wallet without adding it to the device wallet's list, so the device wallet does not treat it as its own until `addESIMWallet` runs.

Deploys a new eSIM wallet, owned by the given device wallet. The device wallet sending the user operation has to be one the registry recognizes.

```ts
const hash = await kokio.eSIMWalletFactory!.deployESIMWalletWithUserOp(
Expand Down
6 changes: 3 additions & 3 deletions docs/sdk/mobile/payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const usdcSymbol = stringToHex("USDC", { size: 32 });
// 0x5553444300000000000000000000000000000000000000000000000000000000
```

The USDC registered on the current deployment is a test token, not Circle's. Its address is on the [deployed addresses](../../contracts/deployments.md) page.
`USDC` on the current deployment is Circle's Base Sepolia USDC. Read any asset's token address with [`resolveAsset`](#resolveasset) rather than hardcoding it.

## registry {#registry}

Expand All @@ -51,10 +51,10 @@ Returns `Promise<Address>`.

## settlementToken {#settlementtoken}

Reads the ERC-20 registered under the `USDC` symbol at configure time.
Reads the token set when the adapter was initialized, the one the vault is meant to end up holding. A purchase does not pay in it: it pays in the token of the asset it names, which `resolveAsset` returns.

```ts
const usdc = await kokio.paymentAdapter!.settlementToken();
const token = await kokio.paymentAdapter!.settlementToken();
```

Returns `Promise<Address>`.
Expand Down
27 changes: 16 additions & 11 deletions docs/sdk/mobile/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ This page gets you from an empty project to a client that can send one.

## What you need {#what-you-need}

- a viem `WalletClient` for the target chain, carrying an `account` and a real RPC URL
- a viem `WalletClient` for the target chain, with a real RPC URL
- the passkey `credentialId` and `rpId` registered for this device
- a Pimlico API key and a gas policy id, used by the bundler and paymaster
- a Pimlico API key, used by the bundler and paymaster, and optionally a Pimlico sponsorship policy id

Passkey signing goes through [`react-native-passkey`](https://github.com/f-23/react-native-passkey) and runs only on a device or simulator that supports WebAuthn. It does not work in a plain Node process.
Passkey signing goes through [`react-native-passkey`](https://github.com/f-23/react-native-passkey), which the app installs itself (`npm install kokio-sdk react-native-passkey`). It runs only on a device or simulator that supports WebAuthn, not in a plain Node process.

## Constructing the client {#constructing-the-client}

Expand All @@ -27,7 +27,6 @@ import { baseSepolia } from "viem/chains";
const rpcUrl = `https://base-sepolia.g.alchemy.com/v2/${alchemyApiKey}`;

const walletClient = createWalletClient({
account: deviceWalletAddress, // presence is checked, this client never signs
chain: baseSepolia,
transport: http(rpcUrl),
});
Expand All @@ -37,15 +36,13 @@ const kokio = new Kokio(
credentialId, // passkey credential id on the device
rpId, // relying party id, your app domain
pimlicoAPIKey,
gasPolicyId,
gasPolicyId, // Pimlico sponsorship policy id, or "" for none
);
```

Two details in that wallet client are easy to get wrong, and both fail later, far from the line that caused them.
The wallet client needs no `account`. The passkey signs everything. The one call that needs an account on it is `deviceWalletFactory.createAccountWithEOA`, which a normal app never uses.

**`account` has to be set.** Without it `getSmartWallet` throws "No signer account found with WalletClient". It is only a presence check. The passkey signs everything and this account is never asked for a signature. Any address the app already holds will do, including the stored device wallet address. The one call that actually spends from it is `deviceWalletFactory.createAccountWithEOA`, which a normal app never uses.

**Give `http()` a real RPC URL.** The SDK reads `client.transport.url` to build the public client it uses for contract reads and nonce lookups. Calling `http()` with no argument leaves that undefined, the reads fall back to Base Sepolia's public endpoint, and its rate limit makes wallet derivation fail intermittently.
**Give `http()` a real RPC URL.** This is easy to get wrong, and it fails later, far from the line that caused it. The SDK reads `client.transport.url` to build the public client it uses for contract reads and nonce lookups. Calling `http()` with no argument leaves that undefined, the reads fall back to Base Sepolia's public endpoint, and its rate limit makes wallet derivation fail intermittently.

## The second construction {#the-second-construction}

Expand Down Expand Up @@ -77,7 +74,7 @@ const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash });
if (!receipt.success) throw new Error("operation reverted");
```

Check `receipt.success`. This is the mistake worth guarding against: an operation whose calls revert is still mined and still returns a receipt, so the await resolving is not proof that anything landed. `receipt.receipt.transactionHash` is the onchain transaction, which is what a block explorer link needs.
A write the contract would refuse throws `ContractRevertError` before it is sent, with the contract's error name in `err.decoded?.errorName`. Still check `receipt.success`: if the chain changes between that check and inclusion, the operation can revert onchain, and it is then mined and still returns a receipt, so the await resolving is not proof that anything landed. `receipt.receipt.transactionHash` is the onchain transaction, which is what a block explorer link needs.

## Switching eSIM wallet {#switching-esim-wallet}

Expand All @@ -91,7 +88,15 @@ session.setESIMWalletAddress(anotherESIMWalletAddress);

## Who deploys the device wallet {#who-deploys}

Normally the backend does, with `admin.deviceWalletFactory.createAccount`. See [backend setup](../backend/setup.md). The app only needs `deviceWalletFactory.createAccountWithEOA` if it holds its own funded EOA, which the wallet client above does not set up.
Either side can. The backend can deploy it with `admin.deviceWalletFactory.createAccount`, see [backend setup](../backend/setup.md). Or the app deploys it with its first sponsored user operation, since the smart account carries its own deployment:

```ts
await session.deviceWallet!.sendUserOperation([]); // an empty operation that only deploys the wallet
```

A wallet deployed that way is not registered yet, so it cannot deploy eSIM wallets. The backend registers it with [`admin.deviceWalletFactory.postCreateAccount`](../backend/device-wallet-factory.md#postcreateaccount).

The app only needs `deviceWalletFactory.createAccountWithEOA` if it holds its own funded EOA, which the wallet client above does not set up.

## Where to go next {#next}

Expand Down
12 changes: 12 additions & 0 deletions docs/sdk/mobile/smart-account.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const account = await kokio.smartAccount.getSmartWallet(

Returns `KokioSmartAccount`, a viem smart account object. Pass it to `getSmartWalletClient`.

The wallet client given to `Kokio` does not need an `account`. The passkey signs, so a client with a chain and a transport is enough.

## getSmartWalletClient {#getsmartwalletclient}

Builds the client that signs with the passkey and sends user operations through Pimlico's bundler and paymaster. Every write on every other mobile surface needs this client, so build it once and reuse it.
Expand All @@ -42,6 +44,16 @@ const smartAccountClient = await kokio.smartAccount.getSmartWalletClient(account

Returns `KokioSmartAccountClient`, a bundler client that can also read contracts directly, since it carries viem's public actions too. Pass it as `smartAccountClient` to a new `Kokio(...)` call so `deviceWallet`, `eSIMWallet` and the rest become available.

Gas is paid by the paymaster at the same endpoint, so the device wallet never needs ETH. The gas policy id given to `Kokio` is optional: pass `""` to send none, or a Pimlico sponsorship policy id to have its rules applied.

To send user operations somewhere other than Pimlico, such as a local bundler in tests, pass `bundlerUrl`. That endpoint must also answer the ERC-7677 paymaster methods.

```ts
const smartAccountClient = await kokio.smartAccount.getSmartWalletClient(account, {
bundlerUrl: "http://127.0.0.1:4337",
});
```

## P256 verifier {#p256-verifier}

`kokio.P256Verifier`
Expand Down
Loading
Loading