# Smart Account Integration

The **native XCN withdrawal** flow accepts EIP-712 intents signed by **deployed ERC-1271 smart accounts** — not just EOAs. Third-party apps built on Safe, Coinbase Smart Wallet, Biconomy, Alchemy AA, ZeroDev, thirdweb, or any AA stack that implements `isValidSignature(bytes32,bytes)` can drive the XCN withdraw flow end-to-end without the owner EOA ever touching the intent.

This page is for **third-party integrators** using smart-account wallets. The [Goliath → Ethereum — Native XCN](/developer-guide/bridge/goliath-to-ethereum.md#native-xcn-withdraw) walkthrough still applies — the endpoints, typed-data fields, poll loop, and status lifecycle are **unchanged**. What's new is how `senderAddress` is resolved (smart-account contract address, not owner EOA), how the intent signature is verified (ERC-1271 on-chain call, not ECDSA recovery), and what the backend accepts as `originTxHash` (the final execution tx, not the `userOpHash`).

{% hint style="info" %}
If you're using an EOA wallet, skip this page — the standard [Native XCN flow](/developer-guide/bridge/goliath-to-ethereum.md#native-xcn-withdraw) is all you need.
{% endhint %}

## Hard Rules

Read all six before wiring code. Missing any one of these stalls the withdrawal.

1. **`senderAddress` = deployed smart-account contract address** on Goliath.
   * Not the owner EOA. Not an ERC-4337 session key. Not a factory address.
   * The bridge enforces that the observed value transfer originated from this exact address.
2. **The smart account must already be deployed** at `senderAddress` on Goliath before signing the intent.
   * Counterfactual (predicted-but-not-yet-deployed) accounts are rejected with `503 SIGNATURE_VERIFICATION_UNAVAILABLE`.
   * **ERC-6492 pre-deploy signatures are not supported.** Deploy first, then sign.
3. **The smart account must implement `isValidSignature(bytes32,bytes) returns (bytes4)`** and return magic `0x1626ba7e` for a valid signature.
   * Every modern AA stack uses this selector — you should not need to change anything wallet-side.
   * The legacy `isValidSignature(bytes,bytes)` variant with magic `0x20c13b0b` is **not** accepted.
4. **`originTxHash` is the final execution tx hash** on Goliath — never the `userOpHash`.
   * `userOpHash` is a 32-byte hex string that's syntactically indistinguishable from a real tx hash, but it's not a transaction, so the bridge can never find it. Your intent will silently stall until it expires.
   * Resolve with `eth_getUserOperationReceipt(userOpHash) → receipt.transactionHash` **before** calling `/bind-origin`.
5. **Direct child call only** — the final execution tx must contain a direct internal `CALL` from `senderAddress` to `relayerWalletAddress` with `value = amountAtomic`.
   * `EntryPoint.handleOps(...)` wrapping that direct call is fine — the bridge walks the call tree.
   * Multi-hop (`smartAccount → Router → relayer`) is **rejected**.
6. **Always read `relayerWalletAddress` from the intent response.** It rotates between networks and at operator discretion. Hardcoded relayer addresses silently strand funds after a rotation.

## How the Bridge Verifies Smart-Account Withdrawals

Only two steps differ from the EOA path:

| Step               | EOA path                                                  | Smart-account path                                                                                                                                                        |
| ------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Intent signature   | `ecrecover(digest, signature) == senderAddress`           | On-chain `eth_call` to `senderAddress.isValidSignature(digest, signature)` — must return `0x1626ba7e`                                                                     |
| Origin-funds proof | Compare `tx.from / tx.to / tx.value` on the bound tx hash | Walk the call tree (Hedera mirror `/api/v1/contracts/results/{txHash}/actions`) for a `CALL` node matching `(senderAddress → relayerWalletAddress, value = amountAtomic)` |

Everything else — typed-data shape, API endpoints, 1-hour security hold, fee schedule, polling, status lifecycle — is identical to the EOA flow.

## Typed-Data Shape (unchanged)

Use the same EIP-712 domain, primary type, and field order as the EOA flow:

```ts
const domain = { name: 'GoliathBridge', version: '1', chainId: 327 }; // 8901 on testnet

const types = {
  XcnWithdrawIntent: [
    { name: 'senderAddress',    type: 'address' },   // smart-account contract address
    { name: 'recipientAddress', type: 'address' },
    { name: 'amountAtomic',     type: 'string'  },
    { name: 'idempotencyKey',   type: 'string'  },
    { name: 'deadline',         type: 'uint256' },
    { name: 'nonce',            type: 'string'  },
  ],
};
```

Signatures from a different domain are rejected with `400 SIGNATURE_DOMAIN_REJECTED`.

## End-to-End Flow

```
1. Deploy smart account on Goliath (one-time, if not already deployed)
2. Sign XcnWithdrawIntent via ERC-1271
3. POST /bridge/xcn-withdraw-intent          → { intentId, relayerWalletAddress, expiresAt }
4. smartAccountClient.sendTransaction({
     to: relayerWalletAddress, value: amountAtomic, data: '0x'
   })                                        → userOpHash (bundler) or tx hash (direct exec)
5. If you received a userOpHash:
     await eth_getUserOperationReceipt(userOpHash)
     → receipt.transactionHash  (this is the final execution tx hash)
6. POST /bridge/xcn-withdraw-intent/bind-origin { intentId, senderAddress, originTxHash }
7. Poll GET /bridge/status?originTxHash=<tx> until COMPLETED
```

Steps 2–6 must complete inside the 30-minute intent window. Step 7 can run as long as needed (\~1 hour because of the security hold).

## Example A — viem + `permissionless.js` (Safe via bundler)

For AA stacks that submit UserOperations through a bundler, the bundler tx hash returned by `eth_getUserOperationReceipt` **is** the execution tx hash.

```ts
import { createSmartAccountClient } from 'permissionless';
import { toSafeSmartAccount } from 'permissionless/accounts';
import { createPublicClient, http, parseEther } from 'viem';
import { goliath } from './chains';

const publicClient = createPublicClient({ chain: goliath, transport: http() });

const safe = await toSafeSmartAccount({
  client: publicClient,
  owners: [ownerAccount],
  saltNonce: 0n,
});

const smartAccountClient = createSmartAccountClient({
  account: safe,
  chain: goliath,
  bundlerTransport: http(BUNDLER_URL),
});

const amountAtomic = parseEther('5000').toString(); // 5000 XCN, 18-decimal wei

// 1) Build and sign the XcnWithdrawIntent typed data via ERC-1271.
const message = {
  senderAddress:    safe.address,          // smart-account contract address
  recipientAddress: RECIPIENT_ON_ETHEREUM,
  amountAtomic,
  idempotencyKey:   crypto.randomUUID(),
  deadline:         BigInt(Math.floor(Date.now() / 1000) + 1800),
  nonce:            Date.now().toString(),
};

const signature = await safe.signTypedData({
  domain: { name: 'GoliathBridge', version: '1', chainId: 327n },
  types: {
    XcnWithdrawIntent: [
      { name: 'senderAddress',    type: 'address' },
      { name: 'recipientAddress', type: 'address' },
      { name: 'amountAtomic',     type: 'string'  },
      { name: 'idempotencyKey',   type: 'string'  },
      { name: 'deadline',         type: 'uint256' },
      { name: 'nonce',            type: 'string'  },
    ],
  },
  primaryType: 'XcnWithdrawIntent',
  message,
});

// 2) Register the intent. Read relayerWalletAddress from the response.
const intentRes = await fetch(
  'https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...message,
      deadline: Number(message.deadline),
      signature,
    }),
  },
);
if (intentRes.status === 503) throw new Error('Transient verification failure — retry');
const { intentId, relayerWalletAddress } = await intentRes.json();

// 3) Send native XCN via the AA client. The bundler wraps this in handleOps(...).
const userOpHash = await smartAccountClient.sendTransaction({
  to:    relayerWalletAddress,
  value: BigInt(amountAtomic),
  data:  '0x',
});

// 4) Resolve the FINAL EXECUTION TX HASH. Never bind a userOpHash.
const userOpReceipt = await publicClient.request({
  method: 'eth_getUserOperationReceipt',
  params: [userOpHash],
});
const executionTxHash = userOpReceipt!.receipt.transactionHash;

// 5) Bind the execution tx to the intent.
await fetch(
  'https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent/bind-origin',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      intentId,
      senderAddress: safe.address,
      originTxHash:  executionTxHash,
    }),
  },
);

// 6) Poll /bridge/status?originTxHash=<executionTxHash> until COMPLETED.
```

## Example B — ethers v6 + `@safe-global/protocol-kit` (direct on-chain)

For Safe accounts that execute on-chain through `executeTransaction` (no bundler), the receipt hash from the final `executeTransaction` **is** the execution tx hash — no `eth_getUserOperationReceipt` call is needed.

```ts
import { ethers } from 'ethers';
import Safe from '@safe-global/protocol-kit';

const provider = new ethers.JsonRpcProvider('https://rpc.goliath.net');
const safe = await Safe.init({
  provider:    'https://rpc.goliath.net',
  signer:      OWNER_PRIVKEY,
  safeAddress: SAFE_ADDRESS,
});

const amountAtomic = ethers.parseEther('5000').toString();

// 1) Sign typed data — protocol-kit routes through the Safe's ERC-1271 path.
const message = {
  senderAddress:    SAFE_ADDRESS,
  recipientAddress: RECIPIENT_ON_ETHEREUM,
  amountAtomic,
  idempotencyKey:   crypto.randomUUID(),
  deadline:         BigInt(Math.floor(Date.now() / 1000) + 1800),
  nonce:            Date.now().toString(),
};

const safeSig = await safe.signTypedData({
  domain:      { name: 'GoliathBridge', version: '1', chainId: 327 },
  types:       { XcnWithdrawIntent: [ /* same field list as Example A */ ] },
  primaryType: 'XcnWithdrawIntent',
  message,
});
const signature = safeSig.data;

// 2) Register intent.
const intentRes = await fetch(
  'https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...message,
      deadline: Number(message.deadline),
      signature,
    }),
  },
);
const { intentId, relayerWalletAddress } = await intentRes.json();

// 3) Route the native-value transfer through the Safe's executeTransaction.
//    The Safe must be the DIRECT caller on the final tx — no forwarders or Routers in between.
const safeTx = await safe.createTransaction({
  transactions: [{
    to:    relayerWalletAddress,
    value: amountAtomic,
    data:  '0x',
  }],
});
const execResult = await safe.executeTransaction(safeTx);
const receipt = await execResult.transactionResponse!.wait();
const executionTxHash = receipt!.hash;

// 4) Bind final execution tx — NOT a userOpHash.
await fetch(
  'https://bridge.goliath.net/api/v1/bridge/xcn-withdraw-intent/bind-origin',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      intentId,
      senderAddress: SAFE_ADDRESS,
      originTxHash:  executionTxHash,
    }),
  },
);
```

## Failure Modes

| HTTP    | Error code                           | Meaning                                                                                           | Client action                                                                                                                      |
| ------- | ------------------------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| 400     | `SIGNATURE_INVALID`                  | ERC-1271 `eth_call` reverted, or returned no data                                                 | Confirm the smart account exposes `isValidSignature(bytes32,bytes)` and the signature is ABI-encoded as the wallet SDK returned it |
| 400     | `SIGNATURE_MISMATCH`                 | ERC-1271 returned a non-magic bytes4 (e.g. `0xffffffff`)                                          | Re-sign; verify `senderAddress` equals the smart-account proxy, not the owner EOA                                                  |
| 400     | `SIGNATURE_DOMAIN_REJECTED`          | Signed against a non-canonical domain                                                             | Use `chainId: 327` (mainnet) or `8901` (testnet), `name: "GoliathBridge"`, `version: "1"`                                          |
| **503** | `SIGNATURE_VERIFICATION_UNAVAILABLE` | Transient RPC error **or** the smart account is not deployed (`eth_getCode(senderAddress) == 0x`) | Back off 1–5 s, deploy the smart account if it isn't yet, then retry                                                               |
| 409     | `DUPLICATE_ORIGIN_TX`                | A different intent already bound this tx hash                                                     | Create a fresh intent                                                                                                              |
| 410     | `INTENT_EXPIRED`                     | Missed the 30 min window                                                                          | Start over from step 2                                                                                                             |

Two extra codes surface only via the operator-driven admin recovery path, listed here for completeness:

| HTTP | Error code          | Meaning                                                             |
| ---- | ------------------- | ------------------------------------------------------------------- |
| 409  | `PROOF_UNAVAILABLE` | Trace data not yet indexed — retry after a short backoff            |
| 422  | `PROOF_MISMATCH`    | Trace shows the wrong sender, recipient, or value — contact support |

## `userOpHash` vs. Execution Tx Hash — the #1 Gotcha

`/bridge/xcn-withdraw-intent/bind-origin` expects a 32-byte hex string in `originTxHash`, and the bridge asks the Goliath relay and the Hedera mirror to find the matching transaction. A `userOpHash` is the same shape (`0x…`, 64 hex chars), but it's **not a transaction** — it's the hash of a UserOperation struct that the bundler inlined inside its own `handleOps(...)` call. No indexer will ever return a tx with that hash.

If you bind a `userOpHash`:

* The intent sits in `PENDING_ORIGIN_TX` and never progresses.
* After 30 minutes it flips to `INTENT_EXPIRED`.
* The 5000 XCN you actually sent is now stuck in the relayer wallet — there's no intent to pair it with.

**Always** resolve to the execution tx hash first:

```ts
const receipt = await publicClient.request({
  method: 'eth_getUserOperationReceipt',
  params: [userOpHash],
});
const executionTxHash = receipt!.receipt.transactionHash;  // <-- this is what you bind
```

If you're executing via Safe's on-chain `executeTransaction` (no bundler), the ethers receipt hash is already the execution tx hash. If you're going through a bundler, you **must** call `eth_getUserOperationReceipt`.

## Common Pitfalls

* **Owner-EOA address in the intent body, smart-account signature in `signature`.** If `senderAddress` is the owner EOA but the signature came from the Safe's ERC-1271 path, you'll get `400 SIGNATURE_MISMATCH`. `senderAddress` must be the smart-account contract address on both the signed intent and the value transfer.
* **Counterfactual Safes.** If the Safe has never been used, `eth_getCode(safeAddress)` returns `0x` and the bridge responds `503 SIGNATURE_VERIFICATION_UNAVAILABLE`. Send a cheap initialization op (or any deploy tx) before binding the intent. ERC-6492 pre-deploy signatures are not supported.
* **Routers and meta-tx forwarders.** `smartAccount → Router → relayer` is rejected as `SENDER_MISMATCH`. The relayer transfer must be a direct child call of the smart account. If you need a router for business logic, build the composed call so the smart account still calls the relayer directly as a leaf.
* **Value-unit mismatch.** `amountAtomic` is the **18-decimal wei** string. Smart-account SDKs (especially thirdweb) sometimes assume tinybars on Hedera-family chains — **do not** apply the 8-decimal scale to XCN. Pass the same integer into the intent `amountAtomic` and into the `value` of the native transfer. See [XCN Decimal Handling](/developer-guide/decimal-handling.md) for background.
* **Hardcoded relayer address.** Every example above reads `relayerWalletAddress` from the intent response. If you hardcode the relayer, a key rotation silently strands every withdrawal after the rotation until you redeploy.

## Next

* [Goliath → Ethereum — Native XCN](/developer-guide/bridge/goliath-to-ethereum.md#native-xcn-withdraw) — the EOA flow this page extends.
* [REST API Reference](/developer-guide/bridge/api-reference.md#post-bridge-xcn-withdraw-intent) — endpoint schemas for `/xcn-withdraw-intent` and `/bind-origin`.
* [XCN Decimal Handling](/developer-guide/decimal-handling.md) — why `amountAtomic` is always 18-decimal wei, never tinybars.
* [Contracts & Events](/developer-guide/bridge/contracts-and-events.md) — Goliath and Ethereum bridge contract addresses.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.goliath.net/developer-guide/bridge/smart-account-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
