Skip to content
Open
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
3 changes: 2 additions & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"require": [
"ts-node/register"
"ts-node/register",
"test/setup.asset-signer.ts"
],
"watch-extensions": [
"ts"
Expand Down
164 changes: 164 additions & 0 deletions docs/asset-signer-wallets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Asset-Signer Wallets

Every MPL Core asset has a deterministic **signer PDA** that can hold SOL, tokens, and even own other assets. Asset-signer wallets let you use this PDA as your active wallet — all CLI commands automatically operate through the PDA.

## Quick Start

```bash
# 1. Create an asset (or use an existing one you own)
mplx core asset create --name "My Vault" --uri "https://example.com/vault"

# 2. Register it as a wallet (auto-detects the owner from on-chain data)
mplx config wallets add vault --asset <assetId>

# 3. Check the PDA info
mplx core asset execute info <assetId>

# 4. Fund the PDA
mplx toolbox sol transfer 0.1 <signerPdaAddress>

# 5. Switch to the asset-signer wallet
mplx config wallets set vault

# 6. Use any command as the PDA
mplx toolbox sol balance
mplx toolbox sol transfer 0.01 <destination>
mplx core asset create --name "PDA Created NFT" --uri "https://example.com/nft"
```

## How It Works

When an asset-signer wallet is active:

1. **`umi.identity`** is set to a noop signer with the PDA's public key — commands build instructions with the PDA as authority naturally
2. **`umi.payer`** is also set to the PDA noop signer — so derived addresses (ATAs, token accounts) resolve correctly
3. **At send time**, the transaction is wrapped in MPL Core's `execute` instruction, which signs on behalf of the PDA on-chain
4. **The real wallet** (asset owner) signs the outer transaction and pays fees via `setFeePayer`

## Wallet Management

### Adding an Asset-Signer Wallet

```bash
mplx config wallets add <name> --asset <assetId>
```

The CLI fetches the asset on-chain, determines the owner, and matches it against your saved wallets. If the owner isn't in your wallet list, you'll be prompted to add it first.

### Listing Wallets

```bash
mplx config wallets list
```

Asset-signer wallets show as `asset-signer` type with the PDA address and linked asset.

### Switching Wallets

```bash
# Switch to asset-signer
mplx config wallets set vault

# Switch back to normal
mplx config wallets set my-wallet
```

### Overriding with -k

Pass `-k` to bypass the asset-signer wallet for a single command:

```bash
# Uses the specified keypair directly, ignores asset-signer
mplx toolbox sol balance -k /path/to/wallet.json
```

## Separate Fee Payer

The on-chain `execute` instruction supports separate authority and fee payer accounts. Use `-p` to have a different wallet pay transaction fees while the asset owner signs the execute:

```bash
mplx toolbox sol transfer 0.01 <destination> -p /path/to/fee-payer.json
```

The asset owner still signs the `execute` instruction. The `-p` wallet only pays the transaction fee.

## Supported Commands

All CLI commands work with asset-signer wallets. The transaction wrapping happens transparently in the send layer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Overbroad support statement conflicts with documented limitations.

Line 87 says all commands work, but Lines 118–125 explicitly document unsupported CPI cases. Reword this to avoid promising universal compatibility.

Suggested doc fix
-All CLI commands work with asset-signer wallets. The transaction wrapping happens transparently in the send layer.
+Most CLI commands work with asset-signer wallets. Transaction wrapping happens transparently in the send layer, with exceptions listed in [CPI Limitations](`#cpi-limitations`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
All CLI commands work with asset-signer wallets. The transaction wrapping happens transparently in the send layer.
Most CLI commands work with asset-signer wallets. Transaction wrapping happens transparently in the send layer, with exceptions listed in [CPI Limitations](`#cpi-limitations`).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/asset-signer-wallets.md` at line 87, Replace the overbroad sentence "All
CLI commands work with asset-signer wallets. The transaction wrapping happens
transparently in the send layer." with a qualified statement that acknowledges
exceptions—e.g., "Most CLI commands work with asset-signer wallets; however,
some CPI cases are not supported and require manual handling." —and add a direct
pointer or inline link to the existing "Unsupported CPI cases" section (the
documented limitations around CPI) so readers know to consult lines describing
unsupported cases (the section currently documenting unsupported CPI scenarios).


### Fully Transparent (no special handling needed)

- **Core**: `asset create`, `asset transfer`, `asset burn`, `asset update`, `collection create`
- **Toolbox SOL**: `balance`, `transfer`, `wrap`, `unwrap`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

wrap/unwrap listed as transparent contradicts SOL wrapping limitation.

Line 92 includes wrap/unwrap as fully transparent, but Line 123 says wSOL wrapping fails in CPI. Keep these sections consistent to prevent false expectations.

Suggested doc fix
-- **Toolbox SOL**: `balance`, `transfer`, `wrap`, `unwrap`
+- **Toolbox SOL**: `balance`, `transfer`
- **SOL wrapping** — creating wrapped SOL (wSOL) token accounts fails in CPI context
+ **SOL wrapping** — `wrap`/`unwrap` flows that create or fund wSOL token accounts fail in CPI context

Also applies to: 123-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/asset-signer-wallets.md` at line 92, The docs currently list the Toolbox
SOL operations "wrap" and "unwrap" as fully transparent but elsewhere note that
wSOL wrapping fails in CPI; update both the "Toolbox SOL" entry and the later
description so they are consistent: change the "wrap"/"unwrap" visibility from
fully transparent to "partially transparent" (or add a caveat) in the Toolbox
SOL line and add a short note next to the wrap/unwrap descriptions explaining
that wSOL wrapping cannot be performed inside CPIs and will fail, referencing
the same wording used in the other section to keep phrasing consistent (look for
the "Toolbox SOL" heading and the wrap/unwrap discussion to edit).

- **Toolbox Token**: `transfer`, `create`, `mint`
- **Toolbox Raw**: `raw --instruction <base64>`
- **Token Metadata**: `transfer`, `create`, `update`
- **Bubblegum**: `nft create` (into existing trees — tree creation itself is a [CPI limitation](#cpi-limitations)), `nft transfer`, `nft burn`, `collection create`
- **Genesis**: `create`, `bucket add-*`, `deposit`, `withdraw`, `claim`, `finalize`, `revoke`
- **Distribution**: `create`, `deposit`, `withdraw`
- **Candy Machine**: `insert`, `withdraw`

### PDA Inspection

```bash
# Show the PDA address and SOL balance for any asset
mplx core asset execute info <assetId>
```

### Raw Instructions

```bash
# Execute arbitrary base64-encoded instructions as the current wallet
# When asset-signer is active, automatically wrapped in execute()
mplx toolbox raw --instruction <base64>
mplx toolbox raw --instruction <ix1> --instruction <ix2>
echo "<base64>" | mplx toolbox raw --stdin
```

## CPI Limitations

Some operations cannot be wrapped in `execute()` due to Solana CPI constraints:

- **Large account creation** — Merkle trees, candy machines (exceed CPI account allocation limits)
- **SOL wrapping** — creating wrapped SOL (wSOL) token accounts fails in CPI context

For these operations, use a normal wallet or create the infrastructure first, then switch to the asset-signer wallet for subsequent operations.

## Building Raw Instructions

The CLI includes serialization helpers for building base64-encoded instructions:

```typescript
import { publicKey } from '@metaplex-foundation/umi'
import { serializeInstruction } from '@metaplex-foundation/cli/lib/execute/deserializeInstruction'

const signerPda = '<PDA address from execute info>'
const destination = '<destination address>'

// System Program SOL transfer
const data = new Uint8Array(12)
const view = new DataView(data.buffer)
view.setUint32(0, 2, true) // Transfer discriminator
view.setBigUint64(4, 1_000_000n, true) // 0.001 SOL

const ix = {
programId: publicKey('11111111111111111111111111111111'),
keys: [
{ pubkey: publicKey(signerPda), isSigner: true, isWritable: true },
{ pubkey: publicKey(destination), isSigner: false, isWritable: true },
],
data,
}

console.log(serializeInstruction(ix))
// Pass the output to: mplx toolbox raw --instruction <base64>
```

### Instruction Binary Format

| Bytes | Field |
|-------|-------|
| 32 | Program ID |
| 2 | Number of accounts (u16 little-endian) |
| 33 per account | 32 bytes pubkey + 1 byte flags (bit 0 = isSigner, bit 1 = isWritable) |
| remaining | Instruction data |
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@
"lint:fix": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .ts --fix",
"postpack": "shx rm -f oclif.manifest.json",
"prepack": "oclif manifest && oclif readme",
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
"test": "npm run test:normal && npm run test:asset-signer",
"test:normal": "mocha --forbid-only \"test/**/*.test.ts\"",
"test:asset-signer": "MPLX_TEST_WALLET_MODE=asset-signer mocha --forbid-only \"test/**/*.test.ts\"",
"version": "oclif readme && git add README.md",
"validator": "CI=1 amman start --config ./.validator.cjs",
"validator:stop": "amman stop",
Expand Down
129 changes: 129 additions & 0 deletions test/WALLET_MODES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Test Wallet Modes

The test suite runs in two wallet modes to verify all commands work with both normal keypairs and asset-signer wallets.

```bash
npm test # Both modes sequentially
npm run test:normal # Normal wallet mode only
npm run test:asset-signer # Asset-signer wallet mode only
```

In asset-signer mode, a root hook (`test/setup.asset-signer.ts`) creates a signing asset, funds its PDA, and writes a temporary config. `runCli` then uses `-c <config>` instead of `-k <keypair>`, so all transactions are wrapped in MPL Core `execute()`.

Infrastructure that can't be created via execute CPI (large account allocations) uses `runCliDirect`, which always uses the normal keypair.

## Test Coverage by Wallet Mode

**Legend:**
- **Y** = runs and passes
- **Skip** = skipped (CPI limitation or authority mismatch)
- **Pending** = pre-existing `describe.skip` (not related to asset-signer)

### Core Commands

| Test | Normal | Asset-Signer |
|---|---|---|
| core asset create (name/uri, collection, custom owner) | Y | Y |
| core asset transfer (standalone, collection, not-owner error) | Y | Y |
| core asset burn (standalone, collection) | Y | Y |
| core asset update | Y | Y |
| core collection create | Y | Y |
| core plugins (add/update on collection and asset) | Y | Y |
| core execute info (PDA address + balance) | Y | Y |

### Asset-Signer Specific

| Test | Normal | Asset-Signer |
|---|---|---|
| Separate fee payer via `-p` | Y | Y |
| Mint cNFT into public tree as PDA | Y | Y |

### Toolbox

| Test | Normal | Asset-Signer |
|---|---|---|
| sol balance (identity + specific address) | Y | Y |
| sol transfer | Y | Y |
| sol wrap | Y | Y |
| sol unwrap | Y | Y |
| token create | Y | Y |
| token mint | Y | Y |
| toolbox raw (execute + error) | Y | Y |

### Token Metadata

| Test | Normal | Asset-Signer |
|---|---|---|
| tm transfer (NFT + pNFT) | Y | Y |
| tm transfer validation errors | Y | Y |
| tm update validation errors | Y | Y |

### Bubblegum

| Test | Normal | Asset-Signer | Notes |
|---|---|---|---|
| bg tree create (8 tests) | Y | Y | Uses `runCliDirect` internally (CPI limitation) |
| bg collection create (9 tests) | Y | Y | No trees involved |
| bg nft create (9 tests) | Y | Skip | Tree authority mismatch — tree owned by wallet, PDA can't mint |
| bg integration (8 tests) | Y | Skip | Tree authority mismatch |
| bg nft burn | Pending | Pending | Pre-existing `describe.skip` |
| bg nft transfer | Pending | Pending | Pre-existing `describe.skip` |
| bg nft update | Pending | Pending | Pre-existing `describe.skip` |

### Candy Machine

| Test | Normal | Asset-Signer | Notes |
|---|---|---|---|
| cm create (3 on-chain tests) | Y | Skip | CM creation is CPI-incompatible (large account) |
| cm create hasGuards (5 unit tests) | Y | Y | Pure unit tests |
| cm full lifecycle (create → insert → withdraw) | Y | Skip | CM authority mismatch |
| cm insert | Y | Skip | CM authority mismatch |
| cm withdraw | Y | Skip | CM authority mismatch |
| cm guard parsing (5 unit tests) | Y | Y | Pure unit tests |

### Genesis

| Test | Normal | Asset-Signer | Notes |
|---|---|---|---|
| genesis create/fetch (7 tests) | Y | Y | Setup uses `runCliDirect` for SOL wrap |
| genesis integration (19 tests) | Y | Skip | Authority mismatch on deposits/finalize |
| genesis launch (12 tests) | Y | Y | Setup uses `runCliDirect` for SOL wrap |
| genesis presale (6 tests) | Y | Skip | Authority mismatch on deposits/claims |
| genesis withdraw (8 tests) | Y | Skip | Authority mismatch on deposits/withdrawals |

### Distribution

| Test | Normal | Asset-Signer | Notes |
|---|---|---|---|
| distro deposit (6 tests) | Y | Skip | Authority mismatch + token account mismatch |
| distro withdraw (8 tests) | Y | Skip | Authority mismatch + token account mismatch |

### Lib (Unit Tests)

| Test | Normal | Asset-Signer |
|---|---|---|
| deserializeInstruction roundtrip (6 tests) | Y | Y |

## Why Some Tests Skip in Asset-Signer Mode

### CPI Limitations

These operations allocate large accounts, which fails when wrapped in `execute()`:

- Merkle tree creation
- Candy machine creation

The `createBubblegumTree` helper and CM creation in test setup use `runCliDirect` to bypass this.

### Authority Mismatch

Resources created with `runCliDirect` (normal wallet) have the wallet as authority. In asset-signer mode, commands run as the PDA, which isn't the authority. This affects:

- bg nft minting (tree authority is wallet, not PDA)
- CM insert/withdraw (CM authority is wallet)
- Genesis deposits/finalize (genesis authority is wallet)
- Distro deposits/withdrawals (distro authority is wallet)

### What IS Tested in Asset-Signer Mode

The asset-signer-specific test creates a **public tree** (anyone can mint) and verifies the PDA can mint a cNFT into it. This confirms bubblegum minting works through execute CPI when authority isn't an issue.
Loading
Loading