diff --git a/docs/smart-accounts/guides/typescript-viem.mdx b/docs/smart-accounts/guides/typescript-viem.mdx
new file mode 100644
index 000000000..05ea7c381
--- /dev/null
+++ b/docs/smart-accounts/guides/typescript-viem.mdx
@@ -0,0 +1,117 @@
+---
+title: TypeScript + Viem
+description: TypeScript and Viem guides for Flare Smart Accounts on Coston2
+keywords:
+ [
+ flare-smart-accounts,
+ viem,
+ typescript,
+ xrpl,
+ account-abstraction,
+ flare-network,
+ ]
+---
+
+import DocCardList from "@theme/DocCardList";
+import DocCard from "@theme/DocCard";
+
+Use [Viem](https://viem.sh/) and the [`xrpl`](https://js.xrpl.org) library to read smart-account state on Flare and drive on-chain actions from an XRPL wallet.
+
+Example code is maintained in the [flare-viem-starter](https://github.com/flare-foundation/flare-viem-starter) repository.
+Read the [Smart Accounts overview](/smart-accounts/overview) and [Custom Instruction](/smart-accounts/custom-instruction) pages first if you are new to the protocol.
+
+## Getting started
+
+Read on-chain state and send arbitrary contract calls through XRPL memo-field instructions.
+
+
+
+## Cross-chain FXRP (LayerZero)
+
+Mint [FXRP](/fxrp/overview) on Flare from XRPL and bridge to Sepolia testnet, or send FXRP back and redeem to XRP.
+
+
+
+## Morpho Blue
+
+Borrow and repay on a Coston2 Morpho Blue test market through a smart account.
+
+
diff --git a/docs/smart-accounts/guides/typescript-viem/06-morpho-setup.mdx b/docs/smart-accounts/guides/typescript-viem/06-morpho-setup.mdx
new file mode 100644
index 000000000..d8af293e2
--- /dev/null
+++ b/docs/smart-accounts/guides/typescript-viem/06-morpho-setup.mdx
@@ -0,0 +1,152 @@
+---
+sidebar_position: 6
+slug: morpho-setup-ts
+title: Morpho Setup
+authors: [nikerzetic]
+description: Fund a smart account and authorize Morpho Blue lending via the MorphoMarketShim on Coston2.
+tags: [intermediate, ethereum, flare-smart-accounts]
+keywords:
+ [
+ ethereum,
+ flare-smart-accounts,
+ evm,
+ flare-network,
+ account-abstraction,
+ morpho,
+ defi,
+ ]
+unlisted: false
+---
+
+import CodeBlock from "@theme/CodeBlock";
+import MorphoSetupScript from "!!raw-loader!/examples/developer-hub-javascript/smart-accounts/morpho/setup.ts";
+
+This guide walks through the one-shot setup script for [Morpho Blue](https://docs.morpho.org/), leveraging a Flare smart account.
+The smart account is the borrower's end-to-end: it holds tokens, supplies collateral, borrows, and later repays via the XRPL memo field [custom instructions](/smart-accounts/custom-instruction).
+
+The setup script does two kinds of work:
+
+1. **Flare EOA transactions** — mint mock ERC-20 collateral and loan tokens to the personal account via permissionless `setBalance`.
+2. **XRPL memo instructions** — up to three payments that approve the `MorphoMarketShim` for both tokens and call `setAuthorization` on Morpho Blue.
+
+:::warning
+Run this script once before the [Morpho Borrow](/smart-accounts/guides/typescript-viem/morpho-borrow-ts) and [Morpho Repay](/smart-accounts/guides/typescript-viem/morpho-repay-ts) guides.
+Each step is idempotent, and skips work that has already been done on the chain.
+:::
+
+The full Morpho cycle lives in the [flare-viem-starter](https://github.com/flare-foundation/flare-viem-starter) repository under `src/morpho/`.
+
+## Prerequisites
+
+- An XRPL testnet wallet (`XRPL_SEED`) registered with the Flare smart-accounts operator and funded with testnet XRP.
+- A Flare-side EOA (`PRIVATE_KEY`) used only to call permissionless mock-token `setBalance` — not to sign Morpho operations on behalf of the smart account.
+- `XRPL_TESTNET_RPC_URL` pointing at an XRPL testnet node.
+- Familiarity with [State Lookup](/smart-accounts/guides/typescript-viem/state-lookup-ts) and [Custom Instruction](/smart-accounts/guides/typescript-viem/custom-instruction-ts).
+
+## Architecture
+
+Morpho Blue markets are identified by a five-field [`MarketParams`](https://docs.morpho.org/get-started/resources/contracts/morpho#market-parameters) tuple (loan token, collateral token, oracle, IRM, LLTV).
+Opening a position normally requires multiple Morpho calls (`supplyCollateral`, `borrow`, and so on).
+Each call encoded in an XRPL memo must stay under the roughly 1024-byte memo cap.
+
+The `MorphoMarketShim` contract pins the market parameters in immutable storage at deploy time.
+That shrinks each memo payload so a bundled operation fits in one payment:
+
+- `supplyAndBorrow` — used in the [Morpho Borrow](/smart-accounts/guides/typescript-viem/morpho-borrow-ts) guide.
+- `repayAndWithdrawCollateral` — used in the [Morpho Repay](/smart-accounts/guides/typescript-viem/morpho-repay-ts) guide.
+
+The smart account remains `onBehalf` for the position; borrowed assets are returned to the smart account, so the position remains self-contained.
+
+### Coston2 Deployment Addresses
+
+| Contract | Address |
+| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
+| Morpho Blue | [`0x8aE0b3CE90F16E88063516f2d88C8ac2ab552d95`](https://coston2-explorer.flare.network/address/0x8aE0b3CE90F16E88063516f2d88C8ac2ab552d95) |
+| Loan token (mock) | [`0x4984B127c3065f4348858fAFdBa020f2c8633905`](https://coston2-explorer.flare.network/address/0x4984B127c3065f4348858fAFdBa020f2c8633905) |
+| Collateral token (mock) | [`0x98bf2F2fF322d5eb61D6aE04Df50856525a85D16`](https://coston2-explorer.flare.network/address/0x98bf2F2fF322d5eb61D6aE04Df50856525a85D16) |
+| Oracle (mock) | [`0x1e80830e9903c839Db803442c976DD2360D47FE0`](https://coston2-explorer.flare.network/address/0x1e80830e9903c839Db803442c976DD2360D47FE0) |
+| IRM (mock) | [`0xDC275701300865D882D44ffe7cb1153535636d1a`](https://coston2-explorer.flare.network/address/0xDC275701300865D882D44ffe7cb1153535636d1a) |
+| MorphoMarketShim | [`0x33d81a1d7986bB3AbAB4F67Ad6117233ADd6F87A`](https://coston2-explorer.flare.network/address/0x33d81a1d7986bB3AbAB4F67Ad6117233ADd6F87A) |
+
+The market uses an LLTV of `86%` (`860000000000000000` in WAD).
+The market ID is `keccak256(abi.encode(marketParams))`, matching Morpho Blue's `MarketParamsLib.id()`.
+
+## Full Script
+
+The script below is sets up the Morpho Blue position by funding the smart account with mock tokens, approving the shim smart contract, and authorizing the shim smart contract on Morpho Blue.
+
+
+ {MorphoSetupScript}
+
+
+## Code Breakdown
+
+The numbered comments in the script above map to these steps.
+
+**1., 2. Configure and connect.**
+Set funding sizes in whole token units (`100` collateral, `1000` loan token), then open an XRPL client and wallet.
+The XRPL wallet controls the personal account; the Flare EOA is only used for mock minting in step 6.
+
+| Token | Units | Purpose |
+| ---------- | ------ | ---------------------------------------------- |
+| Collateral | `100` | Matches the supply size in the borrow script |
+| Loan | `1000` | Buffer for interest across borrow/repay cycles |
+
+**3., 4. Resolve addresses and log them.**
+In parallel, the script looks up the personal account, the memo-only XRP fee, and market decimals.
+It then prints the personal account, operator EOA, market ID, and shim address.
+
+**5. Snapshot before setup.**
+It logs the Morpho position and wallet balances.
+On a first run, borrow shares and position collateral should be zero.
+
+**6. Fund mock tokens on Flare.**
+The `mintMock` function calls permissionless `setBalance` on the Coston2 mock ERC-20s, crediting the personal account.
+This is a normal Flare transaction from the operator EOA without interacting with the smart account.
+
+**7. Approve and authorize the shim.**
+It sends up to 3 XRPL memos: unlimited `approve` for collateral and loan tokens, then `setAuthorization` for Morpho Blue.
+Each action uses its own memo because two calls do not fit under the 1024-byte cap.
+
+**8. Snapshot after setup.**
+Wallet balances should reflect the mint; Morpho position fields stay zero until [Morpho Borrow](/smart-accounts/guides/typescript-viem/morpho-borrow-ts).
+
+## Expected Output
+
+When you run the script, you should see the following output.
+
+```bash
+Personal account: 0x...
+Operator EOA: 0x...
+Morpho market id: 0x...
+Shim address: 0x33d81a1d7986bB3AbAB4F67Ad6117233ADd6F87A
+
+=== Before setup ===
+ position supply (≈loan tokens): 0
+ position borrow (≈loan tokens): 0
+ position collateral: 0
+ ...
+
+Funded smart account with collateral and loan tokens.
+
+[approve-collateral] XRPL transaction hash: ...
+[approve-loan] XRPL transaction hash: ...
+[set-authorization] XRPL transaction hash: ...
+
+=== After setup ===
+ smart-account collateral balance: 100
+ smart-account loan-token balance: 1000
+ ...
+```
+
+You can repeat borrow and repay without re-running the setup, as the loan-token buffer covers interest.
+Re-run setup if allowances are revoked or the loan-token balance is exhausted.
+
+:::tip[What's next]
+
+After you have completed the setup, you can:
+
+- Open a position by running the [Morpho Borrow](/smart-accounts/guides/typescript-viem/morpho-borrow-ts) guide.
+- Close a position by running the [Morpho Repay](/smart-accounts/guides/typescript-viem/morpho-repay-ts) guide.
+
+:::
diff --git a/docs/smart-accounts/guides/typescript-viem/07-morpho-borrow.mdx b/docs/smart-accounts/guides/typescript-viem/07-morpho-borrow.mdx
new file mode 100644
index 000000000..ccc76b5a0
--- /dev/null
+++ b/docs/smart-accounts/guides/typescript-viem/07-morpho-borrow.mdx
@@ -0,0 +1,112 @@
+---
+sidebar_position: 7
+slug: morpho-borrow-ts
+title: Morpho Borrow
+authors: [nikerzetic]
+description: Open a Morpho Blue borrow position from a Flare smart account via supplyAndBorrow.
+tags: [intermediate, ethereum, flare-smart-accounts]
+keywords:
+ [
+ ethereum,
+ flare-smart-accounts,
+ evm,
+ flare-network,
+ account-abstraction,
+ morpho,
+ defi,
+ ]
+unlisted: false
+---
+
+import CodeBlock from "@theme/CodeBlock";
+import MorphoBorrowScript from "!!raw-loader!/examples/developer-hub-javascript/smart-accounts/morpho/borrow.ts";
+
+This guide opens a [Morpho Blue](https://docs.morpho.org/) borrow position from a Flare smart account in a single XRPL memo.
+It calls the `MorphoMarketShim` smart contract `supplyAndBorrow` function, which supplies `100` units of collateral and borrows up to roughly `99%` of the maximum healthy loan amount.
+
+:::info Setup
+Run the [Morpho Setup](/smart-accounts/guides/typescript-viem/morpho-setup-ts) guide first.
+Without funded tokens, shim smart contract approvals, and Morpho authorization, the transaction reverts.
+:::
+
+To close the position follow the [Morpho Repay](/smart-accounts/guides/typescript-viem/morpho-repay-ts) guide.
+
+The full Morpho cycle lives in the [flare-viem-starter](https://github.com/flare-foundation/flare-viem-starter).
+
+## Full Script
+
+To open a borrow position, explore the full script below.
+
+
+ {MorphoBorrowScript}
+
+
+## Code Breakdown
+
+**1. Connect to XRPL.**
+Open an XRPL client and wallet.
+The XRPL wallet controls the personal account.
+Morpho operations are sent as memo-field custom instructions from the smart account, not as Flare EOA (External Owned Account) transactions.
+
+**2. Resolve on-chain inputs in parallel.**
+The script fetches the personal account address, the memo-only XRP fee, market token decimals, and the mock oracle price in parallel.
+
+**3. Set collateral supply size.**
+Collateral is fixed at `100` whole token units, scaled by `collateralDecimals`.
+This matches the collateral funding amount from [Morpho Setup](/smart-accounts/guides/typescript-viem/morpho-setup-ts).
+
+**4. Log addresses.**
+It prints the personal account, operator EOA, Morpho market ID, and shim address for this run.
+
+**5. Snapshot before borrowing.**
+It logs the Morpho position and wallet balances before the borrow.
+On a first borrow after setup, the borrowed shares and the position collateral should be zero while the smart account holds the funded tokens.
+
+**6. Compute borrow size off-chain.**
+The script reads the mock oracle price and applies Morpho Blue's health formula to derive the maximum borrowable amount:
+
+```
+maxBorrowAssets = collateral * oraclePrice * lltv / (oraclePriceScale * WAD)
+```
+
+It then borrows `99%` of that value to leave a small safety margin for interest accrued during the transaction.
+
+**7. Send the supply-and-borrow memo.**
+A single custom instruction executes the `MorphoMarketShim` smart contract `supplyAndBorrow` function.
+
+Bundling supply and borrow inside the shim smart contract keeps the encoded user operation under the XRPL memo size limit.
+
+**8. Snapshot after borrowing.**
+Print the Morpho position and wallet balances after the borrow.
+Expect position collateral to be near `100`, a non-zero borrow balance, and an increased loan-token wallet balance.
+
+## Expected Output
+
+When you run the script, you should see output similar to the following.
+
+```bash
+Personal account: 0x...
+Operator EOA: 0x...
+Morpho market id: 0x...
+Shim address: 0x33d81a1d7986bB3AbAB4F67Ad6117233ADd6F87A
+
+=== Before borrow ===
+ position borrow (≈loan tokens): 0
+ position collateral: 0
+ ...
+
+Oracle price: ...
+Max borrowable: ... → borrowing: ...
+
+[supply-and-borrow] XRPL transaction hash: ...
+[supply-and-borrow] UserOperationExecuted event: ...
+
+=== After borrow ===
+ position borrow (≈loan tokens): ...
+ position collateral: 100
+ ...
+```
+
+:::tip[What's next]
+After you have completed the borrow, you can explore how to [repay the borrow position](/smart-accounts/guides/typescript-viem/morpho-repay-ts).
+:::
diff --git a/docs/smart-accounts/guides/typescript-viem/08-morpho-repay.mdx b/docs/smart-accounts/guides/typescript-viem/08-morpho-repay.mdx
new file mode 100644
index 000000000..71583aa1a
--- /dev/null
+++ b/docs/smart-accounts/guides/typescript-viem/08-morpho-repay.mdx
@@ -0,0 +1,99 @@
+---
+sidebar_position: 8
+slug: morpho-repay-ts
+title: Morpho Repay
+authors: [nikerzetic]
+description: Close a Morpho Blue position from a Flare smart account via repayAndWithdrawCollateral.
+tags: [intermediate, ethereum, flare-smart-accounts]
+keywords:
+ [
+ ethereum,
+ flare-smart-accounts,
+ evm,
+ flare-network,
+ account-abstraction,
+ morpho,
+ defi,
+ ]
+unlisted: false
+---
+
+import CodeBlock from "@theme/CodeBlock";
+import MorphoRepayScript from "!!raw-loader!/examples/developer-hub-javascript/smart-accounts/morpho/repay.ts";
+
+This guide closes a Morpho Blue position that was opened in the [Morpho Borrow](/smart-accounts/guides/typescript-viem/morpho-borrow-ts) guide.
+
+:::info Setup
+It assumes the [Morpho Setup](/smart-accounts/guides/typescript-viem/morpho-setup-ts) guide has already funded the smart account and configured shim approvals plus Morpho authorization.
+:::
+
+## Full Script
+
+To close the position, explore the full script below.
+
+
+ {MorphoRepayScript}
+
+
+## Code Breakdown
+
+**1. Connect to XRPL.**
+The XRPL wallet controls the personal account.
+Morpho operations are sent as memo-field custom instructions from the smart account, not as Flare EOA (Externally Owned Account) transactions.
+
+**2. Resolve on-chain inputs in parallel.**
+The script fetches the personal account address, the memo-only XRP fee, and market token decimals.
+
+**3. Log addresses.**
+It prints the personal account, operator EOA, Morpho market ID, and shim smart contract address for this run.
+
+**4. Snapshot before repayment and early exit.**
+It reads the current Morpho position for the smart account and logs the borrowed shares and position collateral.
+
+**5. Send the repay-and-withdraw memo.**
+A single custom instruction calls `MorphoMarketShim` smart contract `repayAndWithdrawCollateral` function with:
+
+- **`borrowShares`** — repay the full borrowed position in share units (matches what Morpho reports for the smart account).
+- **`collateral`** — withdraw the entire position collateral amount in asset units.
+- **`personalAccount`** — using the personal account address as the `collateralReceiver` so withdrawn collateral returns to the smart account rather than an external address.
+
+Repay uses share-denominated repayment and asset-denominated withdrawal, aligned with Morpho Blue.
+The shim smart contract performs repay and withdraw operations atomically, so the position cannot be left half-closed if one leg fails.
+
+**6. Snapshot after repayment.**
+Log position and wallet balances again.
+Expect the borrowed shares and position collateral to be near zero.
+The loan token balance may be slightly lower than before the borrow cycle due to accrued interest.
+
+## Expected Output
+
+When you run the script, you should see output similar to the following.
+
+```bash
+Personal account: 0x...
+Operator EOA: 0x...
+Morpho market id: 0x...
+Shim address: 0x...
+
+=== Before repay ===
+ position borrow (≈loan tokens): ...
+ position collateral: 100
+ ...
+
+Repaying full position, borrowShares: ...
+
+[repay-and-withdraw] XRPL transaction hash: ...
+[repay-and-withdraw] UserOperationExecuted event: ...
+
+=== After repay + withdraw ===
+ position borrow (≈loan tokens): 0
+ position collateral: 0
+ ...
+```
+
+:::tip[What's next]
+After you have completed the repayment, you can explore how to [open a new borrow position](/smart-accounts/guides/typescript-viem/morpho-borrow-ts) again.
+
+You can open and repay, re-running the setup while the loan-token buffer covers interest.
+Re-run [setup](/smart-accounts/guides/typescript-viem/morpho-setup-ts) if allowances are revoked or the loan-token balance is exhausted.
+:::
diff --git a/examples/developer-hub-javascript/smart-accounts/abis/ERC20.ts b/examples/developer-hub-javascript/smart-accounts/abis/ERC20.ts
new file mode 100644
index 000000000..1870ec75e
--- /dev/null
+++ b/examples/developer-hub-javascript/smart-accounts/abis/ERC20.ts
@@ -0,0 +1,344 @@
+export const abi = [
+ {
+ inputs: [
+ {
+ internalType: "string",
+ name: "name",
+ type: "string",
+ },
+ {
+ internalType: "string",
+ name: "symbol",
+ type: "string",
+ },
+ ],
+ stateMutability: "nonpayable",
+ type: "constructor",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "spender",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "allowance",
+ type: "uint256",
+ },
+ {
+ internalType: "uint256",
+ name: "needed",
+ type: "uint256",
+ },
+ ],
+ name: "ERC20InsufficientAllowance",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "sender",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "balance",
+ type: "uint256",
+ },
+ {
+ internalType: "uint256",
+ name: "needed",
+ type: "uint256",
+ },
+ ],
+ name: "ERC20InsufficientBalance",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "approver",
+ type: "address",
+ },
+ ],
+ name: "ERC20InvalidApprover",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "receiver",
+ type: "address",
+ },
+ ],
+ name: "ERC20InvalidReceiver",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "sender",
+ type: "address",
+ },
+ ],
+ name: "ERC20InvalidSender",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "spender",
+ type: "address",
+ },
+ ],
+ name: "ERC20InvalidSpender",
+ type: "error",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "spender",
+ type: "address",
+ },
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "value",
+ type: "uint256",
+ },
+ ],
+ name: "Approval",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "value",
+ type: "uint256",
+ },
+ ],
+ name: "Transfer",
+ type: "event",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "spender",
+ type: "address",
+ },
+ ],
+ name: "allowance",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "spender",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "value",
+ type: "uint256",
+ },
+ ],
+ name: "approve",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "account",
+ type: "address",
+ },
+ ],
+ name: "balanceOf",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "decimals",
+ outputs: [
+ {
+ internalType: "uint8",
+ name: "",
+ type: "uint8",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "amount",
+ type: "uint256",
+ },
+ ],
+ name: "mint",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "name",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "symbol",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "totalSupply",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "value",
+ type: "uint256",
+ },
+ ],
+ name: "transfer",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "value",
+ type: "uint256",
+ },
+ ],
+ name: "transferFrom",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+];
diff --git a/examples/developer-hub-javascript/smart-accounts/abis/MorphoBlue.ts b/examples/developer-hub-javascript/smart-accounts/abis/MorphoBlue.ts
new file mode 100644
index 000000000..c9795c51e
--- /dev/null
+++ b/examples/developer-hub-javascript/smart-accounts/abis/MorphoBlue.ts
@@ -0,0 +1,150 @@
+export const abi = [
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: "address", name: "loanToken", type: "address" },
+ { internalType: "address", name: "collateralToken", type: "address" },
+ { internalType: "address", name: "oracle", type: "address" },
+ { internalType: "address", name: "irm", type: "address" },
+ { internalType: "uint256", name: "lltv", type: "uint256" },
+ ],
+ internalType: "struct MarketParams",
+ name: "marketParams",
+ type: "tuple",
+ },
+ { internalType: "uint256", name: "assets", type: "uint256" },
+ { internalType: "address", name: "onBehalf", type: "address" },
+ { internalType: "bytes", name: "data", type: "bytes" },
+ ],
+ name: "supplyCollateral",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: "address", name: "loanToken", type: "address" },
+ { internalType: "address", name: "collateralToken", type: "address" },
+ { internalType: "address", name: "oracle", type: "address" },
+ { internalType: "address", name: "irm", type: "address" },
+ { internalType: "uint256", name: "lltv", type: "uint256" },
+ ],
+ internalType: "struct MarketParams",
+ name: "marketParams",
+ type: "tuple",
+ },
+ { internalType: "uint256", name: "assets", type: "uint256" },
+ { internalType: "uint256", name: "shares", type: "uint256" },
+ { internalType: "address", name: "onBehalf", type: "address" },
+ { internalType: "address", name: "receiver", type: "address" },
+ ],
+ name: "borrow",
+ outputs: [
+ { internalType: "uint256", name: "assetsBorrowed", type: "uint256" },
+ { internalType: "uint256", name: "sharesBorrowed", type: "uint256" },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: "address", name: "loanToken", type: "address" },
+ { internalType: "address", name: "collateralToken", type: "address" },
+ { internalType: "address", name: "oracle", type: "address" },
+ { internalType: "address", name: "irm", type: "address" },
+ { internalType: "uint256", name: "lltv", type: "uint256" },
+ ],
+ internalType: "struct MarketParams",
+ name: "marketParams",
+ type: "tuple",
+ },
+ { internalType: "uint256", name: "assets", type: "uint256" },
+ { internalType: "uint256", name: "shares", type: "uint256" },
+ { internalType: "address", name: "onBehalf", type: "address" },
+ { internalType: "bytes", name: "data", type: "bytes" },
+ ],
+ name: "repay",
+ outputs: [
+ { internalType: "uint256", name: "assetsRepaid", type: "uint256" },
+ { internalType: "uint256", name: "sharesRepaid", type: "uint256" },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: "address", name: "loanToken", type: "address" },
+ { internalType: "address", name: "collateralToken", type: "address" },
+ { internalType: "address", name: "oracle", type: "address" },
+ { internalType: "address", name: "irm", type: "address" },
+ { internalType: "uint256", name: "lltv", type: "uint256" },
+ ],
+ internalType: "struct MarketParams",
+ name: "marketParams",
+ type: "tuple",
+ },
+ { internalType: "uint256", name: "assets", type: "uint256" },
+ { internalType: "address", name: "onBehalf", type: "address" },
+ { internalType: "address", name: "receiver", type: "address" },
+ ],
+ name: "withdrawCollateral",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ { internalType: "Id", name: "id", type: "bytes32" },
+ { internalType: "address", name: "user", type: "address" },
+ ],
+ name: "position",
+ outputs: [
+ { internalType: "uint256", name: "supplyShares", type: "uint256" },
+ { internalType: "uint128", name: "borrowShares", type: "uint128" },
+ { internalType: "uint128", name: "collateral", type: "uint128" },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ { internalType: "address", name: "authorized", type: "address" },
+ { internalType: "bool", name: "newIsAuthorized", type: "bool" },
+ ],
+ name: "setAuthorization",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ { internalType: "address", name: "authorizer", type: "address" },
+ { internalType: "address", name: "authorized", type: "address" },
+ ],
+ name: "isAuthorized",
+ outputs: [{ internalType: "bool", name: "", type: "bool" }],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [{ internalType: "Id", name: "id", type: "bytes32" }],
+ name: "market",
+ outputs: [
+ { internalType: "uint128", name: "totalSupplyAssets", type: "uint128" },
+ { internalType: "uint128", name: "totalSupplyShares", type: "uint128" },
+ { internalType: "uint128", name: "totalBorrowAssets", type: "uint128" },
+ { internalType: "uint128", name: "totalBorrowShares", type: "uint128" },
+ { internalType: "uint128", name: "lastUpdate", type: "uint128" },
+ { internalType: "uint128", name: "fee", type: "uint128" },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+];
diff --git a/examples/developer-hub-javascript/smart-accounts/abis/MorphoMarketShim.ts b/examples/developer-hub-javascript/smart-accounts/abis/MorphoMarketShim.ts
new file mode 100644
index 000000000..463d12d26
--- /dev/null
+++ b/examples/developer-hub-javascript/smart-accounts/abis/MorphoMarketShim.ts
@@ -0,0 +1,104 @@
+export const abi = [
+ {
+ inputs: [
+ { internalType: "address", name: "_morpho", type: "address" },
+ {
+ components: [
+ { internalType: "address", name: "loanToken", type: "address" },
+ { internalType: "address", name: "collateralToken", type: "address" },
+ { internalType: "address", name: "oracle", type: "address" },
+ { internalType: "address", name: "irm", type: "address" },
+ { internalType: "uint256", name: "lltv", type: "uint256" },
+ ],
+ internalType: "struct MarketParams",
+ name: "params",
+ type: "tuple",
+ },
+ ],
+ stateMutability: "nonpayable",
+ type: "constructor",
+ },
+ { inputs: [], name: "ApproveFailed", type: "error" },
+ { inputs: [], name: "TransferFromFailed", type: "error" },
+ { inputs: [], name: "UnauthorizedCallback", type: "error" },
+ {
+ inputs: [],
+ name: "collateralToken",
+ outputs: [{ internalType: "address", name: "", type: "address" }],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "irm",
+ outputs: [{ internalType: "address", name: "", type: "address" }],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "lltv",
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "loanToken",
+ outputs: [{ internalType: "address", name: "", type: "address" }],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "morpho",
+ outputs: [{ internalType: "contract IMorpho", name: "", type: "address" }],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ { internalType: "uint256", name: "assets", type: "uint256" },
+ { internalType: "bytes", name: "data", type: "bytes" },
+ ],
+ name: "onMorphoRepay",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "oracle",
+ outputs: [{ internalType: "address", name: "", type: "address" }],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ { internalType: "uint256", name: "repayShares", type: "uint256" },
+ { internalType: "uint256", name: "withdrawAssets", type: "uint256" },
+ { internalType: "address", name: "collateralReceiver", type: "address" },
+ ],
+ name: "repayAndWithdrawCollateral",
+ outputs: [
+ { internalType: "uint256", name: "assetsRepaid", type: "uint256" },
+ { internalType: "uint256", name: "sharesRepaid", type: "uint256" },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ { internalType: "uint256", name: "collateralAssets", type: "uint256" },
+ { internalType: "uint256", name: "borrowAssets", type: "uint256" },
+ { internalType: "address", name: "borrowReceiver", type: "address" },
+ ],
+ name: "supplyAndBorrow",
+ outputs: [
+ { internalType: "uint256", name: "assetsBorrowed", type: "uint256" },
+ { internalType: "uint256", name: "sharesBorrowed", type: "uint256" },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+] as const;
diff --git a/examples/developer-hub-javascript/smart-accounts/morpho/borrow.ts b/examples/developer-hub-javascript/smart-accounts/morpho/borrow.ts
new file mode 100644
index 000000000..877e84771
--- /dev/null
+++ b/examples/developer-hub-javascript/smart-accounts/morpho/borrow.ts
@@ -0,0 +1,108 @@
+import { encodeFunctionData, formatUnits } from "viem";
+import { Client, Wallet } from "xrpl";
+import { abi as MorphoMarketShimAbi } from "../abis/MorphoMarketShim";
+import { account, publicClient } from "../utils/client";
+import {
+ getPersonalAccountAddress,
+ sendMemoFieldInstruction,
+} from "../utils/smart-accounts";
+import { computeDirectMintingPaymentAmountXrp } from "../utils/fassets";
+import {
+ LLTV,
+ MORPHO_MARKET_SHIM_ADDRESS,
+ ORACLE_ABI,
+ ORACLE_ADDRESS,
+ WAD,
+ fetchMarketDecimals,
+ getAndLogState,
+ marketId,
+} from "./utils";
+
+// NOTE:(Nik) Run src/morpho/setup.ts once before this script — it funds the
+// smart account with mock collateral and loan tokens, approves the shim for
+// both, and authorizes the shim on Morpho. Without setup, this script's op
+// memo will revert.
+//
+// Architecture: the smart account is the actor end-to-end. A MorphoMarketShim
+// pins the 5-field MarketParams in immutable state, so each shim call fits
+// inside an XRPL memo (~842 bytes vs the 1024-byte cap). The shim's
+// `supplyAndBorrow` bundles both Morpho ops on-chain so a full open step is
+// a single memo — two separate Morpho ops in one memo would exceed the cap.
+// Borrowed loan tokens go to the smart account itself (receiver=personalAccount),
+// keeping the position fully self-contained.
+//
+// Run src/morpho/repay.ts afterwards to close the position.
+async function main() {
+ // 1. Connect to XRPL testnet with the wallet that controls the personal account.
+ const xrplClient = new Client(process.env.XRPL_TESTNET_RPC_URL!);
+ const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);
+
+ // 2. Resolve the personal account, memo-only XRP fee, market decimals, and oracle price in parallel.
+ const [personalAccount, memoOnlyAmountXrp, marketDecimals, oraclePrice] =
+ await Promise.all([
+ getPersonalAccountAddress(xrplWallet.address),
+ computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: 0 }),
+ fetchMarketDecimals(),
+ publicClient.readContract({
+ address: ORACLE_ADDRESS,
+ abi: ORACLE_ABI,
+ functionName: "price",
+ }),
+ ]);
+ const { loanDecimals, collateralDecimals, oraclePriceScale } = marketDecimals;
+
+ // 3. Fix collateral supply at 100 whole units (scaled by token decimals).
+ const collateralAssets = 100n * 10n ** BigInt(collateralDecimals);
+
+ // 4. Log the addresses involved in this run.
+ console.log("Personal account:", personalAccount, "\n");
+ console.log("Operator EOA: ", account.address, "\n");
+ console.log("Morpho market id:", marketId, "\n");
+ console.log("Shim address: ", MORPHO_MARKET_SHIM_ADDRESS, "\n");
+
+ // 5. Snapshot Morpho position and token balances before opening the position.
+ await getAndLogState("Before borrow", personalAccount, marketDecimals);
+
+ // 6. Compute max borrow off-chain via Morpho Blue's health formula, then borrow 99% for a safety margin.
+ const maxBorrowAssets =
+ (collateralAssets * oraclePrice * LLTV) / (oraclePriceScale * WAD);
+ const borrowAssets = (maxBorrowAssets * 99n) / 100n;
+ console.log("Oracle price:", oraclePrice.toString());
+ console.log(
+ "Max borrowable:",
+ formatUnits(maxBorrowAssets, loanDecimals),
+ "→ borrowing:",
+ formatUnits(borrowAssets, loanDecimals),
+ "\n",
+ );
+
+ // 7. Supply collateral and borrow loan tokens in one XRPL memo via the shim.
+ await sendMemoFieldInstruction({
+ label: "supply-and-borrow",
+ calls: [
+ {
+ target: MORPHO_MARKET_SHIM_ADDRESS,
+ value: 0n,
+ data: encodeFunctionData({
+ abi: MorphoMarketShimAbi,
+ functionName: "supplyAndBorrow",
+ args: [collateralAssets, borrowAssets, personalAccount],
+ }),
+ },
+ ],
+ amountXrp: memoOnlyAmountXrp,
+ personalAccount,
+ xrplClient,
+ xrplWallet,
+ });
+
+ // 8. Snapshot state again to confirm the position opened and loan tokens arrived.
+ await getAndLogState("After borrow", personalAccount, marketDecimals);
+}
+
+void main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
diff --git a/examples/developer-hub-javascript/smart-accounts/morpho/repay.ts b/examples/developer-hub-javascript/smart-accounts/morpho/repay.ts
new file mode 100644
index 000000000..2e22a4a57
--- /dev/null
+++ b/examples/developer-hub-javascript/smart-accounts/morpho/repay.ts
@@ -0,0 +1,92 @@
+import { encodeFunctionData } from "viem";
+import { Client, Wallet } from "xrpl";
+import { abi as MorphoMarketShimAbi } from "../abis/MorphoMarketShim";
+import { account } from "../utils/client";
+import {
+ getPersonalAccountAddress,
+ sendMemoFieldInstruction,
+} from "../utils/smart-accounts";
+import { computeDirectMintingPaymentAmountXrp } from "../utils/fassets";
+import {
+ MORPHO_MARKET_SHIM_ADDRESS,
+ fetchMarketDecimals,
+ getAndLogState,
+ marketId,
+} from "./utils";
+
+// NOTE:(Nik) Run after src/morpho/borrow.ts has opened a position. Assumes
+// src/morpho/setup.ts has already funded the smart account and configured
+// shim approvals + authorization on Morpho. Closes the smart account's
+// Morpho position via the shim's `repayAndWithdrawCollateral` (share-
+// denominated repay + withdraw, atomic) with the collateral routed back to
+// the smart account. See borrow.ts for architecture overview.
+async function main() {
+ // 1. Connect to XRPL — wallet signs memos that drive the smart account; operations are not standalone Flare EOA transactions.
+ const xrplClient = new Client(process.env.XRPL_TESTNET_RPC_URL!);
+ const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);
+
+ // 2. Resolve on-chain inputs in parallel — personal account, XRPL memo fee, Morpho token decimals for logging.
+ const [personalAccount, memoOnlyAmountXrp, marketDecimals] =
+ await Promise.all([
+ getPersonalAccountAddress(xrplWallet.address),
+ computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: 0 }),
+ fetchMarketDecimals(),
+ ]);
+
+ // 3. Log addresses for this run.
+ console.log("Personal account:", personalAccount, "\n");
+ console.log("Operator EOA: ", account.address, "\n");
+ console.log("Morpho market id:", marketId, "\n");
+ console.log("Shim address: ", MORPHO_MARKET_SHIM_ADDRESS, "\n");
+
+ // 4. Snapshot before repay — exit early if there is no open position.
+ const { borrowShares, collateral } = await getAndLogState(
+ "Before repay",
+ personalAccount,
+ marketDecimals,
+ );
+
+ if (borrowShares === 0n && collateral === 0n) {
+ console.log("Nothing to repay or withdraw. Exiting.");
+ return;
+ }
+
+ console.log(
+ "Repaying full position, borrowShares:",
+ borrowShares.toString(),
+ "\n",
+ );
+ // 5. Send repay-and-withdraw memo — shim repays full borrow shares, withdraws all collateral atomically (receiver = smart account).
+ await sendMemoFieldInstruction({
+ label: "repay-and-withdraw",
+ calls: [
+ {
+ target: MORPHO_MARKET_SHIM_ADDRESS,
+ value: 0n,
+ data: encodeFunctionData({
+ abi: MorphoMarketShimAbi,
+ functionName: "repayAndWithdrawCollateral",
+ args: [borrowShares, collateral, personalAccount],
+ }),
+ },
+ ],
+ amountXrp: memoOnlyAmountXrp,
+ personalAccount,
+ xrplClient,
+ xrplWallet,
+ });
+
+ // 6. Snapshot after repay — expect borrow shares and position collateral near zero (loan-token balance reflects interest paid).
+ await getAndLogState(
+ "After repay + withdraw",
+ personalAccount,
+ marketDecimals,
+ );
+}
+
+void main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
diff --git a/examples/developer-hub-javascript/smart-accounts/morpho/setup.ts b/examples/developer-hub-javascript/smart-accounts/morpho/setup.ts
new file mode 100644
index 000000000..4eef58d65
--- /dev/null
+++ b/examples/developer-hub-javascript/smart-accounts/morpho/setup.ts
@@ -0,0 +1,76 @@
+import { Client, Wallet } from "xrpl";
+import { account } from "../utils/client";
+import { getPersonalAccountAddress } from "../utils/smart-accounts";
+import { computeDirectMintingPaymentAmountXrp } from "../utils/fassets";
+import {
+ COLLATERAL_TOKEN_ADDRESS,
+ LOAN_TOKEN_ADDRESS,
+ MORPHO_MARKET_SHIM_ADDRESS,
+ ensureShimSetup,
+ fetchMarketDecimals,
+ getAndLogState,
+ marketId,
+ mintMock,
+} from "./utils";
+
+// One-shot init for the Morpho cycle: fund the smart account with mock tokens,
+// then approve and authorize the MorphoMarketShim. Safe to re-run — each step
+// reads on-chain state and skips work already done.
+async function main() {
+ // 1. Choose how many whole token units to fund (scaled by decimals in step 6).
+ // 100 collateral matches borrow.ts; 1000 loan token covers interest across cycles.
+ const collateralFundingUnits = 100n;
+ const loanFundingUnits = 1000n;
+
+ // 2. Connect to XRPL testnet with the wallet that controls the personal account.
+ const xrplClient = new Client(process.env.XRPL_TESTNET_RPC_URL!);
+ const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);
+
+ // 3. Resolve the personal account, memo-only XRP fee, and market decimals in parallel.
+ const [personalAccount, memoOnlyAmountXrp, marketDecimals] =
+ await Promise.all([
+ getPersonalAccountAddress(xrplWallet.address),
+ computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: 0 }),
+ fetchMarketDecimals(),
+ ]);
+
+ // 4. Log the addresses involved in this run.
+ console.log("Personal account:", personalAccount, "\n");
+ console.log("Operator EOA: ", account.address, "\n");
+ console.log("Morpho market id:", marketId, "\n");
+ console.log("Shim address: ", MORPHO_MARKET_SHIM_ADDRESS, "\n");
+
+ // 5. Snapshot Morpho position and token balances before funding and setup.
+ await getAndLogState("Before setup", personalAccount, marketDecimals);
+
+ // 6. Fund the smart account with mock collateral and loan tokens (EOA-signed on Flare).
+ await mintMock(
+ COLLATERAL_TOKEN_ADDRESS,
+ personalAccount,
+ collateralFundingUnits * 10n ** BigInt(marketDecimals.collateralDecimals),
+ );
+ await mintMock(
+ LOAN_TOKEN_ADDRESS,
+ personalAccount,
+ loanFundingUnits * 10n ** BigInt(marketDecimals.loanDecimals),
+ );
+ console.log("Funded smart account with collateral and loan tokens.\n");
+
+ // 7. Approve the shim for both tokens and authorize it on Morpho Blue (XRPL memos).
+ await ensureShimSetup({
+ personalAccount,
+ xrplClient,
+ xrplWallet,
+ amountXrp: memoOnlyAmountXrp,
+ });
+
+ // 8. Snapshot state again to confirm balances and that setup memos succeeded.
+ await getAndLogState("After setup", personalAccount, marketDecimals);
+}
+
+void main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
diff --git a/examples/developer-hub-javascript/smart-accounts/morpho/utils.ts b/examples/developer-hub-javascript/smart-accounts/morpho/utils.ts
new file mode 100644
index 000000000..7e739041e
--- /dev/null
+++ b/examples/developer-hub-javascript/smart-accounts/morpho/utils.ts
@@ -0,0 +1,279 @@
+import {
+ encodeAbiParameters,
+ encodeFunctionData,
+ formatUnits,
+ keccak256,
+ parseAbi,
+ type Address,
+} from "viem";
+import type { Client, Wallet } from "xrpl";
+import { abi as ERC20Abi } from "../abis/ERC20";
+import { abi as MorphoBlueAbi } from "../abis/MorphoBlue";
+import { account, publicClient, walletClient } from "../utils/client";
+import { sendMemoFieldInstruction } from "../utils/smart-accounts";
+
+// Coston2 Morpho Blue test stack (mock tokens, mock oracle, mock IRM).
+export const MORPHO_BLUE_ADDRESS =
+ "0x8aE0b3CE90F16E88063516f2d88C8ac2ab552d95" as Address;
+export const LOAN_TOKEN_ADDRESS =
+ "0x4984B127c3065f4348858fAFdBa020f2c8633905" as Address;
+export const COLLATERAL_TOKEN_ADDRESS =
+ "0x98bf2F2fF322d5eb61D6aE04Df50856525a85D16" as Address;
+export const ORACLE_ADDRESS =
+ "0x1e80830e9903c839Db803442c976DD2360D47FE0" as Address;
+export const IRM_ADDRESS =
+ "0xDC275701300865D882D44ffe7cb1153535636d1a" as Address;
+export const LLTV = 860000000000000000n; // 86 %
+
+// MorphoMarketShim deployed on Coston2 — see flare-hardhat-starter/scripts/morpho/deploys.ts.
+export const MORPHO_MARKET_SHIM_ADDRESS =
+ "0x33d81a1d7986bB3AbAB4F67Ad6117233ADd6F87A" as Address;
+
+export const WAD = 10n ** 18n;
+export const MAX_UINT256 = 2n ** 256n - 1n;
+// Allowance >= this counts as "approved unlimited" for setup-skip purposes.
+const APPROVAL_THRESHOLD = 2n ** 255n;
+
+// The mock collateral and loan tokens expose an unauthenticated setBalance(account, amount).
+export const MOCK_ERC20_ABI = parseAbi([
+ "function setBalance(address account, uint256 amount)",
+]);
+export const ORACLE_ABI = parseAbi(["function price() view returns (uint256)"]);
+export const POSITION_ABI = parseAbi([
+ "function position(bytes32 id, address user) view returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral)",
+]);
+
+export const marketParams = {
+ loanToken: LOAN_TOKEN_ADDRESS,
+ collateralToken: COLLATERAL_TOKEN_ADDRESS,
+ oracle: ORACLE_ADDRESS,
+ irm: IRM_ADDRESS,
+ lltv: LLTV,
+} as const;
+
+// Id = keccak256(abi.encode(marketParams)) — matches MarketParamsLib.id() in morpho-blue.
+export const marketId = keccak256(
+ encodeAbiParameters(
+ [
+ {
+ type: "tuple",
+ components: [
+ { name: "loanToken", type: "address" },
+ { name: "collateralToken", type: "address" },
+ { name: "oracle", type: "address" },
+ { name: "irm", type: "address" },
+ { name: "lltv", type: "uint256" },
+ ],
+ },
+ ],
+ [marketParams],
+ ),
+);
+
+// Reads the loan and collateral token decimals and derives the oracle's price
+// scale per Morpho Blue's IOracle convention: 10 ** (36 + loanDecimals - collateralDecimals).
+// The Coston2 example mock tokens are minimal and don't implement decimals() —
+// fall back to 18 there (matches the values they're actually scaled to).
+async function readDecimalsOrDefault(tokenAddress: Address): Promise {
+ try {
+ return (await publicClient.readContract({
+ address: tokenAddress,
+ abi: ERC20Abi,
+ functionName: "decimals",
+ })) as number;
+ } catch {
+ return 18;
+ }
+}
+
+export async function fetchMarketDecimals() {
+ const [loanDecimals, collateralDecimals] = await Promise.all([
+ readDecimalsOrDefault(LOAN_TOKEN_ADDRESS),
+ readDecimalsOrDefault(COLLATERAL_TOKEN_ADDRESS),
+ ]);
+ const oraclePriceScale =
+ 10n ** (36n + BigInt(loanDecimals) - BigInt(collateralDecimals));
+ return { loanDecimals, collateralDecimals, oraclePriceScale };
+}
+
+export async function mintMock(
+ tokenAddress: Address,
+ recipient: Address,
+ amount: bigint,
+) {
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: tokenAddress,
+ abi: MOCK_ERC20_ABI,
+ functionName: "setBalance",
+ args: [recipient, amount],
+ });
+ const transactionHash = await walletClient.writeContract(request);
+ await publicClient.waitForTransactionReceipt({ hash: transactionHash });
+}
+
+// Brings the smart account → shim authorization state to "fully set up": the
+// shim is approved for both tokens and authorized on Morpho. Each action is
+// emitted as its own XRPL memo because two of these calls do not fit in a
+// single 1024-byte memo together (a single call already runs ~810 bytes).
+// Idempotent: reads on-chain state first and skips any action already done.
+export async function ensureShimSetup({
+ personalAccount,
+ xrplClient,
+ xrplWallet,
+ amountXrp,
+}: {
+ personalAccount: Address;
+ xrplClient: Client;
+ xrplWallet: Wallet;
+ amountXrp: number;
+}) {
+ const [collateralAllowance, loanAllowance, morphoAuthorized] =
+ (await Promise.all([
+ publicClient.readContract({
+ address: COLLATERAL_TOKEN_ADDRESS,
+ abi: ERC20Abi,
+ functionName: "allowance",
+ args: [personalAccount, MORPHO_MARKET_SHIM_ADDRESS],
+ }),
+ publicClient.readContract({
+ address: LOAN_TOKEN_ADDRESS,
+ abi: ERC20Abi,
+ functionName: "allowance",
+ args: [personalAccount, MORPHO_MARKET_SHIM_ADDRESS],
+ }),
+ publicClient.readContract({
+ address: MORPHO_BLUE_ADDRESS,
+ abi: MorphoBlueAbi,
+ functionName: "isAuthorized",
+ args: [personalAccount, MORPHO_MARKET_SHIM_ADDRESS],
+ }),
+ ])) as [bigint, bigint, boolean];
+
+ if (
+ collateralAllowance >= APPROVAL_THRESHOLD &&
+ loanAllowance >= APPROVAL_THRESHOLD &&
+ morphoAuthorized
+ ) {
+ console.log(
+ "Smart account → shim setup already complete — skipping setup memos.\n",
+ );
+ return;
+ }
+
+ const sharedMemoFields = {
+ amountXrp,
+ personalAccount,
+ xrplClient,
+ xrplWallet,
+ };
+
+ if (collateralAllowance < APPROVAL_THRESHOLD) {
+ await sendMemoFieldInstruction({
+ ...sharedMemoFields,
+ label: "approve-collateral",
+ calls: [
+ {
+ target: COLLATERAL_TOKEN_ADDRESS,
+ value: 0n,
+ data: encodeFunctionData({
+ abi: ERC20Abi,
+ functionName: "approve",
+ args: [MORPHO_MARKET_SHIM_ADDRESS, MAX_UINT256],
+ }),
+ },
+ ],
+ });
+ }
+ if (loanAllowance < APPROVAL_THRESHOLD) {
+ await sendMemoFieldInstruction({
+ ...sharedMemoFields,
+ label: "approve-loan",
+ calls: [
+ {
+ target: LOAN_TOKEN_ADDRESS,
+ value: 0n,
+ data: encodeFunctionData({
+ abi: ERC20Abi,
+ functionName: "approve",
+ args: [MORPHO_MARKET_SHIM_ADDRESS, MAX_UINT256],
+ }),
+ },
+ ],
+ });
+ }
+ if (!morphoAuthorized) {
+ await sendMemoFieldInstruction({
+ ...sharedMemoFields,
+ label: "set-authorization",
+ calls: [
+ {
+ target: MORPHO_BLUE_ADDRESS,
+ value: 0n,
+ data: encodeFunctionData({
+ abi: MorphoBlueAbi,
+ functionName: "setAuthorization",
+ args: [MORPHO_MARKET_SHIM_ADDRESS, true],
+ }),
+ },
+ ],
+ });
+ }
+}
+
+export async function getAndLogState(
+ label: string,
+ smartAccount: Address,
+ marketDecimals: { loanDecimals: number; collateralDecimals: number },
+) {
+ const [position, collateralBalance, loanBalance] = await Promise.all([
+ publicClient.readContract({
+ address: MORPHO_BLUE_ADDRESS,
+ abi: POSITION_ABI,
+ functionName: "position",
+ args: [marketId, smartAccount],
+ }),
+ publicClient.readContract({
+ address: COLLATERAL_TOKEN_ADDRESS,
+ abi: ERC20Abi,
+ functionName: "balanceOf",
+ args: [smartAccount],
+ }) as Promise,
+ publicClient.readContract({
+ address: LOAN_TOKEN_ADDRESS,
+ abi: ERC20Abi,
+ functionName: "balanceOf",
+ args: [smartAccount],
+ }) as Promise,
+ ]);
+ const [supplyShares, borrowShares, collateral] = position;
+
+ // Morpho Blue's VIRTUAL_SHARES = 1e6: at market init `shares = assets * 1e6`,
+ // so loan-token-denominated supply/borrow are recovered by formatUnits with
+ // (loanDecimals + 6). The conversion drifts slightly as interest accrues.
+ const sharesScale = marketDecimals.loanDecimals + 6;
+ console.log(`=== ${label} ===`);
+ console.log(
+ " position supply (≈loan tokens): ",
+ formatUnits(supplyShares, sharesScale),
+ );
+ console.log(
+ " position borrow (≈loan tokens): ",
+ formatUnits(borrowShares, sharesScale),
+ );
+ console.log(
+ " position collateral: ",
+ formatUnits(collateral, marketDecimals.collateralDecimals),
+ );
+ console.log(
+ " smart-account collateral balance:",
+ formatUnits(collateralBalance, marketDecimals.collateralDecimals),
+ );
+ console.log(
+ " smart-account loan-token balance:",
+ formatUnits(loanBalance, marketDecimals.loanDecimals),
+ );
+ console.log("");
+
+ return { supplyShares, borrowShares, collateral };
+}
diff --git a/sidebars.ts b/sidebars.ts
index 9b8f3b788..d9e0a3d4b 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -197,8 +197,8 @@ const sidebars: SidebarsConfig = {
label: "TypeScript + Viem",
collapsed: true,
link: {
- slug: "/smart-accounts/guides/typescript-viem",
- type: "generated-index",
+ type: "doc",
+ id: "smart-accounts/guides/typescript-viem",
},
items: [
{