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
53 changes: 48 additions & 5 deletions .agents/skills/oneshot-embedded-wallet/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: oneshot-embedded-wallet
description: >-
Integrate the 1Shot embedded wallet (OWS Host Layer) with @1shotapi/ows-provider.
Use when embedding wallet.1shotapi.com, wiring OWSProxy, EIP-1193, credentials,
or custom RPC such as configure / focusWallet / addAsset / createAccount for
or custom RPC such as configure / focusWallet / addAsset / createAccount / onramp / bridge for
theming, host-driven focus mode, tracked assets, and first-party Safari create.
license: MIT
metadata:
Expand All @@ -16,6 +16,8 @@ metadata:

Teach an agent how to embed the **1Shot Wallet** Branding Layer from a Host Layer app using `@1shotapi/ows-provider`.

> Use of 1Shot-hosted public infrastructure is subject to the [Public Infrastructure Terms](https://1shotapi.com/legal/public-infrastructure-terms) and [Acceptable Use Policy](https://1shotapi.com/legal/acceptable-use-policy). By accessing or using those hosted services, you agree to those terms.

```
Host (your dapp) @1shotapi/ows-provider → OWSProxy
└── Branding iframe https://wallet.1shotapi.com/
Expand Down Expand Up @@ -84,7 +86,7 @@ await proxy.rpc("configure", options);
| `features.disableCredentials` | boolean | Hide Credentials tab; default `false`. Host credential flows still work |
| `features.disableDelegations` | boolean | Hide Delegations tab; default `false`. Host delegation flows still work |
| `features.allowedChains` | `string[]` (hex `0x…` chain ids) | Restrict Network dropdown to these catalog chains; omit or `[]` ⇒ all enabled |
| `destinationUrl` | string \| null | URL to receive transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/relayer/get-started/overview) (≤256 chars). `null` or `""` clears |
| `destinationUrl` | string \| null | URL to receive transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/embedded-wallet/webhooks) (≤256 chars). `null` or `""` clears |
| `copy.productName` | string | titles / chrome |
| `copy.tagline` | string | supporting line |
| `copy.connect.title` | string | connect modal title |
Expand Down Expand Up @@ -195,7 +197,7 @@ Host-controlled shell modes. Callers (not end users) switch between **General**
```typescript
// Lock to one chain + ERC-20 (or other) asset
await proxy.rpc("focusWallet", {
chainId: "0x4cef52", // Arc Testnet
chainId: "0x13b2", // Arc
assetAddress: "0x3600000000000000000000000000000000000000", // USDC
});
proxy.showWallet();
Expand All @@ -220,7 +222,7 @@ Propose a tracked **ERC-20** for the Balances tab. The wallet resolves the token

```typescript
await proxy.rpc("addAsset", {
chainId: "0x4cef52", // Arc Testnet
chainId: "0x13b2", // Arc
assetAddress: "0x3600000000000000000000000000000000000000", // USDC
});
proxy.showWallet();
Expand All @@ -236,7 +238,7 @@ Users can also add assets from the Balances tab without a host RPC. The Balances

## Custom RPC — `createAccount`

Used by the first-party **`/create/`** host page (Safari passkey create). Hosts embedding the wallet normally do **not** call this — the branding layer opens `/create/` itself when needed.
Used by the first-party **`/create/`** host page (Safari passkey create). Hosts embedding the wallet normally do **not** call this — the branding layer opens `/create/` itself when needed. Not exposed as a playground button.

```typescript
const result = await proxy.rpc("createAccount");
Expand All @@ -250,13 +252,52 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" });
|--------|--------|--------|
| `createAccount` | `{ accountName?: string }` optional | Runs setup create (passkey + relayer register); returns credential id |

## Custom RPC — `onramp`

Opens Circle fiat onramp fullscreen inside the Branding Layer for the unlocked EVM address.

```typescript
await proxy.rpc("onramp", {
chainId: 8453, // optional — decimal chain id for catalog scoping
amount: "50", // optional — amount hint when supported by the kit
});
// or: await proxy.rpc("onramp", {});
```

| Method | Params | Behavior |
|--------|--------|----------|
| `onramp` | `{ chainId?: number, amount?: string }` | Shows wallet, mounts Circle AppKit onramp; session minted via Relayer `POST /wallet/onramp` |

Returns `{ ok: true }` when the user closes the onramp view. The Relayer holds the Circle kit key; the browser only receives a single-use session. Inline iframe requires Circle CSP allowlisting of the wallet origin; for local/ngrok testing the branding layer honors `localStorage.setItem("circlePopup", "true")` and uses AppKit `openWindow` instead.

## Custom RPC — `bridge`

Opens the gasless CCTP USDC bridge (native TokenMessengerV2 + Circle Forwarding Service, submitted through the 1Shot relayer). Locked wallet → error. Close before confirm → `OwsUserRejectedError`. Omit `sourceChainId` to use the session chain.

```typescript
await proxy.rpc("bridge", {
amount: "10.50", // optional human USDC
sourceChainId: 8453, // optional decimal; omit → session chain
destinationChainId: 1, // optional; omit → user picks
});
// or: await proxy.rpc("bridge", {});
```

| Method | Params | Behavior |
|--------|--------|----------|
| `bridge` | `{ amount?: string, sourceChainId?: number, destinationChainId?: number }` | Shows wallet, opens CCTP bridge for native USDC on a relayer CCTP source. Dest must be a same-network CCTP chain. |

Returns `{ ok: true, burnTxHash, forwardTxHash? }` when the source burn is submitted (and destination mint if Iris has completed). The user pays the relayer USDC fee (same path as Send); destination mint is Circle’s Forwarding Service — no dest-chain signature and no native gas.

## Other Host APIs

| API | Use |
|-----|-----|
| `proxy.ethereum.request(...)` | EIP-1193 (accounts, sign, chain, …) |
| `proxy.ethereum.on` / `removeListener` | Branding→Host EIP-1193 notifications (`chainChanged`, `accountsChanged` via `ows:eip1193`) |
| `proxy.credentials.*` | OID4 offer / present (when enabled in wallet) |
| `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call |
| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, `onramp`, `bridge`, …) |
| `proxy.analytics.on(listener)` / `.on(name, listener)` / `.off(listener)` | Branding→Host product analytics (`ows:analytics`) |
| `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call |
| `proxy.rpc(method, params)` | Custom Branding RPC (`configure`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, …) |
Expand Down Expand Up @@ -308,3 +349,5 @@ include a live Analytics panel fed by `proxy.analytics.on` (filter by `name`).
- Theme with `configure`; do not ask integrators to fork CSS for basic brand colors / product name.
- Use `focusWallet` / `unfocusWallet` for host-driven single-asset flows; do not expose mode switching in the wallet UI.
- Use `addAsset` when the host wants a lasting Balances entry; expect a confirm modal (contrast with `focusWallet`).
- Use `onramp` (or the in-wallet Buy button) for fiat → crypto; do not put the Circle kit key in the Host or Branding Layer.
- Use `bridge` (or the in-wallet Bridge button) for gasless CCTP USDC; do not require native gas or dest-chain `receiveMessage`.
3 changes: 3 additions & 0 deletions .github/workflows/Deploy Dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ jobs:
gcloud --quiet auth configure-docker $GAR_LOCATION-docker.pkg.dev

- name: Build Wallet
env:
CLOUDSMITH_TOKEN: ${{ secrets.CLOUDSMITH_TOKEN }}
run: |-
docker build \
--secret id=cloudsmith_token,env=CLOUDSMITH_TOKEN \
--tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:$GITHUB_SHA" \
--tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:${GITHUB_REF##*/}" \
--tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:latest" \
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/Deploy Prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ jobs:
gcloud --quiet auth configure-docker $GAR_LOCATION-docker.pkg.dev

- name: Build Wallet
env:
CLOUDSMITH_TOKEN: ${{ secrets.CLOUDSMITH_TOKEN }}
run: |-
docker build \
--secret id=cloudsmith_token,env=CLOUDSMITH_TOKEN \
--tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:$GITHUB_SHA" \
--tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:${GITHUB_REF##*/}" \
--tag "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$PRODUCT_NAME/oneshot-wallet:latest" \
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.DS_Store

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
Expand Down Expand Up @@ -154,3 +155,4 @@ oneshot-wallet-*-deploy
oneshot-wallet-*-deploy.pub
*-deploy
*-deploy.pub
Circle Onramp Kit Beta testing.md
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@crcl-main:registry=https://npm.cloudsmith.io/circle/common-private/
//npm.cloudsmith.io/circle/common-private/:_authToken=${CLOUDSMITH_TOKEN}
32 changes: 31 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ Prefer **clean code over backwards compatibility**. Do not add legacy redirects,
| `src/lib/types/domain/` | Domain DTOs (e.g. `KnownAsset`, `TrackedAsset`, `WalletConfig`) |
| `src/lib/types/events/` | Domain event classes (one file each) |
| `src/lib/interfaces/{business,data,utils}/` | Layer interfaces |
| `src/lib/implementations/{business,data,utils}/` | Layer implementations |
| `src/lib/implementations/{business,data,utils}/` | Layer implementations (`CircleProvider` for AppKit onramp) |
| `src/assets/` | Static media only (SVGs, images) |

Test Host Layer: `host/` (`npm run dev:host`). Browser extension: `extension/` (`npm run dev:extension`) — see [`extension/README.md`](extension/README.md). Branding / host config via Host RPC `configure`, not in-wallet debug knobs.

Fiat onramp: Asset Details **Buy** and host RPC `onramp({ chainId?, amount? })` open `OnrampView` (Circle AppKit). Sessions come from Relayer `POST /wallet/onramp` — never put the Circle kit key in this SPA. Default UI is `mountIframe`; for local/ngrok before Circle CSP allowlisting, set `localStorage.circlePopup = "true"` to use `openWindow` (prefetch session, then a sync click). Cloudsmith: set `CLOUDSMITH_TOKEN` before `npm install` (see README).

CCTP bridge: Asset Details **Bridge** (native USDC with `useCCTPBridge`) and host RPC `bridge({ amount?, sourceChainId?, destinationChainId? })` open `CCTPBridge`. Source omit → session chain. Burns via `TokenMessengerV2.depositForBurnWithHook` + `cctp-forward` hook through the EIP-7710 relayer (same USDC fee as Send). Destination mint is Circle’s Forwarding Service — no dest-chain signature, no native gas, no BridgeKit.

### Form validation UX

Primary submit actions (e.g. Send in `TransferTokensModal`) stay **disabled until every required field is valid**. Do not leave the button enabled and only reject on click. Empty fields show no error text; invalid non-empty input shows inline errors; the CTA enables only when the whole form is ready.
Expand All @@ -48,6 +52,32 @@ When adding or changing UI strings or host-tunable options:
- **Data:** `IKnownAssetRepository`, `ITrackedAssetRepository`, `IOneshotRelayerRepository` (`src/lib`) and their implementations
- **Business:** services that orchestrate domain logic (add as needed)

### Injectable classes (constructor DI)

Prefer **direct constructor parameter properties** for injectable services/utils — not an `XXXOptions` bag. Call sites pass dependencies positionally; implementations use `this.chainRepository` (etc.), never `this.options.*`.

```ts
// Prefer
export class BridgeService implements IBridgeService {
constructor(
protected readonly chainRepository: IChainRepository,
protected readonly knownAssetRepository: IKnownAssetRepository,
protected readonly circleRepository: ICircleRepository,
protected readonly transactionUtils: ITransactionUtils,
protected readonly cctpUtils: ICCTPUtils,
protected readonly blockchain: IBlockchainProvider,
) {}
}

// Avoid
export type BridgeServiceOptions = { chainRepository: IChainRepository; /* … */ };
export class BridgeService {
constructor(private readonly options: BridgeServiceOptions) {}
}
```

Wire at the composition root (e.g. `WalletProvider`) with positional args: `new BridgeService(chainRepository, knownAssetRepository, …)`.

`IOneshotRelayerRepository.sendTransaction` owns prepare + passkey sign + broadcast (interim: `eth_sendRawTransaction`). Host EIP-1193 sends go SignHelper → branding `approveAndSignTransaction` (ConfirmTransfer / SendTransaction consent) → relayer. In-wallet Send uses `TransferTokensModal` → `WalletProvider.sendTransaction` → relayer, then `SentTransactionModal` (hash + explorer link). Host-driven sends do not show that confirmation — the host surfaces the hash itself.

## Branded types
Expand Down
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ WORKDIR /app

COPY package.json package-lock.json ./
COPY host/package.json ./host/
RUN npm ci
COPY .npmrc ./

# Circle @crcl-main/* requires CLOUDSMITH_TOKEN BuildKit secret
RUN --mount=type=secret,id=cloudsmith_token \
CLOUDSMITH_TOKEN="$(cat /run/secrets/cloudsmith_token)" \
npm ci \
&& rm -f .npmrc

COPY index.html vite.config.ts tsconfig.json tsconfig.node.json components.json ./
COPY src ./src
Expand Down
Loading