Skip to content

Commit 22b597a

Browse files
committed
fix(docs): improve custom instruction overview and guide
1 parent 0857b0a commit 22b597a

2 files changed

Lines changed: 23 additions & 37 deletions

File tree

docs/smart-accounts/3-custom-instruction.mdx

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,17 @@ A destination tag forces [FAssets direct minting](/fassets/direct-minting) to cr
3434

3535
The custom instruction payload has two layers: the outer EIP-4337 [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation) carried in the XRPL memo, and the inner [`executeUserOp(Call[])`](/smart-accounts/reference/IPersonalAccount#executeuserop) that the personal account runs once the controller dispatches it.
3636

37-
### `PackedUserOperation`
38-
39-
```solidity
40-
struct PackedUserOperation {
41-
address sender;
42-
uint256 nonce;
43-
bytes initCode;
44-
bytes callData;
45-
bytes32 accountGasLimits;
46-
uint256 preVerificationGas;
47-
bytes32 gasFees;
48-
bytes paymasterAndData;
49-
bytes signature;
50-
}
51-
```
52-
53-
Only three fields are required for Flare Smart Accounts:
37+
Only three fields from the [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation) struct are required for Flare Smart Accounts:
5438

5539
- `sender` **must** equal the address of the personal account derived from the XRPL sender.
56-
Use [`getPersonalAccount(xrplOwner)`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) to look it up — the address is deterministic, so you can fetch it before the account is even deployed.
57-
- `nonce` **must** equal the personal account's current nonce returned by [`getNonce(personalAccount)`](/smart-accounts/reference/IMasterAccountController#getnonce).
40+
Use [`getPersonalAccount`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) to look it up — the address is deterministic, so you can fetch it before the account is even deployed.
41+
- `nonce` **must** equal the personal account's current nonce returned by [`getNonce`](/smart-accounts/reference/IMasterAccountController#getnonce).
5842
The nonce auto-increments on every successful execution to prevent replay.
5943
- `callData` is the calldata that the controller invokes on the personal account.
6044
In practice, this is `abi.encodeCall(IPersonalAccount.executeUserOp, (calls))` — anything else either reverts or is rejected by the personal account's `onlyController` modifier.
6145

6246
The remaining fields are not validated on-chain and can be left empty.
63-
Authorization comes from the XRPL signature on the `Payment` transaction itself: only the XRPL key for `sender`'s `xrplOwner` can deliver the memo.
47+
Authorization comes from the XRPL signature on the `Payment` XRPL payment transaction itself: only the XRPL key for `sender`'s `xrplOwner` can deliver the memo.
6448
If the personal account has pinned an executor via [`getExecutor`](/smart-accounts/reference/IMasterAccountController#getexecutor), only that executor is permitted to relay the mint.
6549

6650
### `executeUserOp` and the `Call` struct
@@ -69,22 +53,24 @@ The personal account's [`executeUserOp(Call[])`](/smart-accounts/reference/IPers
6953

7054
```solidity
7155
struct Call {
72-
address target;
73-
uint256 value;
74-
bytes data;
56+
address target;
57+
uint256 value;
58+
bytes data;
7559
}
7660
7761
function executeUserOp(Call[] calldata _calls) external payable;
7862
```
7963

8064
Each call is dispatched in array order with the personal account as `msg.sender`.
81-
If any call reverts, the whole user operation reverts with [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) — partial execution is not possible.
65+
If any call reverts, the whole user operation reverts with [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) — partial execution is not possible.
8266

83-
`executeUserOp` is `payable`, so the user operation can forward FLR alongside the calls.
84-
Funding works the usual way: send FLR to the personal account address (it is deterministic; see [State Lookup](/smart-accounts/guides/state-lookup-ts#personal-account-of-an-xrpl-address)) and the calls can spend it via `Call.value`.
67+
`executeUserOp` is `payable`, so the user operation can forward native tokens (e.g. FLR) alongside the calls.
68+
Funding works the usual way: send FLR to the personal account address.
8569

8670
### Building `callData` in TypeScript
8771

72+
You can build the `callData` in TypeScript using the [`encodeFunctionData`](https://viem.sh/docs/contract/encodeFunctionData#encodefunctiondata) function from `viem` library:
73+
8874
```typescript
8975
import { encodeFunctionData } from "viem";
9076

@@ -107,7 +93,7 @@ const callData = encodeFunctionData({
10793
});
10894
```
10995

110-
The encoded `callData` becomes the `callData` field of the `PackedUserOperation` placed in the XRPL memo.
96+
The encoded `callData` becomes the `callData` field of the `PackedUserOperation` placed in the XRPL payment memo field.
11197

11298
## Replay protection
11399

@@ -121,7 +107,7 @@ The whole pipeline is atomic with respect to the user operation:
121107
- If `sender` does not match the personal account, the call reverts with [`InvalidSender`](/smart-accounts/reference/IMasterAccountController#invalidsender).
122108
- If `nonce` is not the expected value, it reverts with [`InvalidNonce`](/smart-accounts/reference/IMasterAccountController#invalidnonce).
123109
- If the memo body has the wrong length for its instruction ID, it reverts with [`InvalidMemoData`](/smart-accounts/reference/IMasterAccountController#invalidmemodata); an unrecognized instruction byte reverts with [`InvalidInstructionId`](/smart-accounts/reference/IMasterAccountController#invalidinstructionid).
124-
- If any inner call reverts, the personal account surfaces it as [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) and the entire user operation reverts.
110+
- If any inner call reverts, the personal account surfaces it as [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) and the entire user operation reverts.
125111

126112
Because the FXRP transfer is performed before the memo is decoded, **the mint succeeds even if the user operation reverts** — see [`DirectMintingExecuted`](/smart-accounts/reference/IMasterAccountController#directmintingexecuted).
127-
The freshly minted FXRP lands in the personal account, and the user can recover by either re-submitting a fixed user operation (after advancing the nonce with the `0xE1` memo, which emits [`NonceIncreased`](/smart-accounts/reference/IMasterAccountController#nonceincreased)) or sweeping the FXRP via standard FAsset instructions.
113+
The freshly minted FXRP lands in the personal account, and the user can recover by either re-submitting a fixed user operation or transferring the FXRP to another address via standard [FAssets instructions](/smart-accounts/fasset-instructions).

docs/smart-accounts/guides/03-custom-instruction.mdx renamed to docs/smart-accounts/guides/02-custom-instruction.mdx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ import CodeBlock from "@theme/CodeBlock";
2121
import CustomInstructionsScript from "!!raw-loader!/examples/developer-hub-javascript/smart-accounts/custom-instructions.ts";
2222

2323
The [Custom Instruction overview](/smart-accounts/custom-instruction) explains how Flare smart accounts replay an [EIP-4337](https://eips.ethereum.org/EIPS/eip-4337) `PackedUserOperation` carried in an XRPL `Payment` memo.
24-
This guide walks through a TypeScript script that builds the user operation with Viem, sends a payment transaction with the user operation on XRPL, and waits for the [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted) event on Flare.
24+
This guide walks through a TypeScript script that builds the user operation with Viem library, sends a payment transaction with the user operation on XRPL, and waits for the [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted) event on Flare.
2525

2626
The script executes three calls on three example smart contracts on the Flare blockchain.
2727
Because the XRPL memo field is capped at roughly `1024` bytes, the calls are split into two user operations sent in sequence.
2828

29-
A prerequisite for the user operation to be executed is that the personal account holds enough native to cover the call values.
29+
A prerequisite for the user operation to be executed is that the personal account holds enough native tokens to cover the call values.
3030
The script calls forward a total of `2` C2FLR (Coston2 Flare native tokens), so fund the personal account from the [Flare faucet](https://faucet.flare.network/coston2) before running it.
3131
The [State Lookup guide](/smart-accounts/guides/state-lookup-ts#personal-account-of-an-xrpl-address) shows how to derive the personal account address.
3232

33-
The full code is available on [GitHub](https://github.com/flare-foundation/flare-viem-starter).
33+
The full code is available on [GitHub](https://github.com/flare-foundation/flare-viem-starter/blob/main/src/custom-instructions.ts).
3434

3535
## Contracts
3636

@@ -184,7 +184,7 @@ const pinNoticeCalls: Call[] = [
184184

185185
## Personal account and nonce
186186

187-
The personal account address is deterministic, so we can fetch it before the account is even deployed by calling [`getPersonalAccount`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) on the `MasterAccountController`.
187+
The personal account address is deterministic, so we can fetch it before the account is even deployed by calling [`getPersonalAccount`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) on the [`MasterAccountController`](/smart-accounts/reference/IMasterAccountController).
188188
Before encoding the user operation, we also need the current nonce from [`getNonce`](/smart-accounts/reference/IMasterAccountController#getnonce).
189189
Each successful execution increments it, so passing an invalid value reverts to [`InvalidNonce`](/smart-accounts/reference/IMasterAccountController#invalidnonce).
190190

@@ -201,7 +201,7 @@ export async function getNonce(personalAccount: Address): Promise<bigint> {
201201

202202
## Encoding the user operation
203203

204-
The XRPL memo carries a 10-byte header followed by an ABI-encoded [`PackedUserOperation`](/smart-accounts/custom-instruction#packeduseroperation):
204+
The XRPL memo carries a 10-byte header followed by an ABI-encoded [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation):
205205

206206
- 1st byte: custom instruction command ID `0xff`
207207
- 2nd byte: `walletId` — a one-byte wallet identifier assigned by Flare; we use `0`
@@ -365,8 +365,8 @@ const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);
365365

366366
## Waiting for execution
367367

368-
Once the operator bridges the instruction from XRPL to Flare, the personal account runs `executeUserOp` and the `MasterAccountController` emits [`UserOperationExecuted(personalAccount, nonce)`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted).
369-
We watch for that event with Viem's [`watchContractEvent`](https://viem.sh/docs/contract/watchContractEvent#watchcontractevent), filtering on the personal account and the nonce we submitted:
368+
Once the operator bridges the instruction from XRPL to Flare, the personal account runs `executeUserOp` and the `MasterAccountController` emits [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted) event.
369+
We watch for that event with Viem's [`watchContractEvent`](https://viem.sh/docs/contract/watchContractEvent#watchcontractevent) function, filtering on the personal account and the nonce we submitted:
370370

371371
```typescript
372372
export async function waitForUserOperationExecuted({
@@ -406,7 +406,7 @@ export async function waitForUserOperationExecuted({
406406

407407
The bridging is handled by the [Flare Data Connector](/fdc/overview), which caps the round trip at 180 seconds.
408408

409-
If any inner call reverts, the whole user operation reverts with [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) and the nonce does not advance.
409+
If any inner call reverts, the whole user operation reverts with [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) and the nonce does not increment.
410410
The FXRP transfer, however, is performed before the memo is decoded, so the mint succeeds even when the user operation reverts — see [`DirectMintingExecuted`](/smart-accounts/reference/IMasterAccountController#directmintingexecuted).
411411

412412
## Full script

0 commit comments

Comments
 (0)