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
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from "./messari";
export * from "./pyth";
export * from "./moonwell";
export * from "./morpho";
export * from "./multisig";
export * from "./opensea";
export * from "./spl";
export * from "./superfluid";
Expand Down
141 changes: 141 additions & 0 deletions typescript/agentkit/src/action-providers/multisig/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Multisig Action Provider

Enable AgentKit agents to participate in multi-agent multisig wallets.

## Overview

This action provider allows AgentKit agents to:
- Sign digests for external multisig coordination
- Sign Safe (Gnosis Safe) transaction hashes
- Share their public key with multisig coordinators

## Use Cases

### Multi-Agent Treasuries

Multiple AI agents from different providers can share a multisig wallet:

```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ AgentKit │ │ aibtc │ │Claw Cash │
│ Agent │ │ Agent │ │ Agent │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────────────┼───────────────┘
┌──────▼──────┐
│ 2-of-3 │
│ Multisig │
│ (Safe) │
└─────────────┘
```

### Cross-Provider Coordination

The coordination flow:
1. Each agent registers their public key with a coordinator
2. Coordinator generates a multisig address
3. Anyone can fund the multisig
4. When spending, coordinator creates a transaction proposal
5. Each agent validates and signs their portion
6. Coordinator assembles signatures and broadcasts

## Actions

### `sign_digest`

Signs a raw 32-byte digest for external coordination protocols.

```typescript
const result = await agent.run({
action: "sign_digest",
args: {
digest: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
});
```

**Security:** Only sign digests after validating the full transaction details from the coordinator.

### `sign_safe_transaction`

Signs a Safe (Gnosis Safe) transaction hash.

```typescript
const result = await agent.run({
action: "sign_safe_transaction",
args: {
safeAddress: "0x...",
safeTxHash: "a1b2c3d4..."
}
});
```

### `get_multisig_pubkey`

Gets the agent's public key for registering with a multisig.

```typescript
const result = await agent.run({
action: "get_multisig_pubkey",
args: {}
});
```

## Integration Example

With [Agent Multisig Coordination API](https://github.com/aetos53t/agent-multisig-api):

```typescript
import { AgentKit, multisigActionProvider } from "@coinbase/agentkit";

// Initialize agent with multisig actions
const agent = new AgentKit({
actionProviders: [multisigActionProvider()]
});

// 1. Get public key for registration
const pubkeyResult = await agent.run({ action: "get_multisig_pubkey" });

// 2. Register with coordination API
await fetch("https://api.agentmultisig.dev/v1/agents", {
method: "POST",
body: JSON.stringify({
id: "my-agent",
publicKey: pubkeyResult.address,
provider: "agentkit"
})
});

// 3. When a proposal needs signing, sign the digest
const signResult = await agent.run({
action: "sign_digest",
args: { digest: proposalSighash }
});

// 4. Submit signature to coordinator
await fetch(`https://api.agentmultisig.dev/v1/proposals/${id}/sign`, {
method: "POST",
body: JSON.stringify({
agentId: "my-agent",
signature: signResult.signature
})
});
```

## Security Considerations

1. **Never blind sign**: Always validate the full transaction before signing a digest
2. **Verify coordinators**: Only work with trusted coordination protocols
3. **Check transaction details**: Ensure inputs, outputs, and amounts are correct
4. **Use threshold signing**: M-of-N schemes protect against single point of failure

## Supported Networks

Currently supports EVM networks (Ethereum, Base, Arbitrum, etc.). Solana support planned.

## Related

- [Agent Multisig Coordination API](https://github.com/aetos53t/agent-multisig-api) - Coordination layer
- [Safe Protocol](https://docs.safe.global/) - EVM multisig standard
- [aibtc MCP Server](https://github.com/aibtcdev/aibtc-mcp-server) - Bitcoin agent integration
2 changes: 2 additions & 0 deletions typescript/agentkit/src/action-providers/multisig/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { MultisigActionProvider, multisigActionProvider } from "./multisigActionProvider";
export { SignDigestSchema, SignSafeTransactionSchema, GetPublicKeySchema } from "./schemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { multisigActionProvider, MultisigActionProvider } from "./multisigActionProvider";
import { EvmWalletProvider } from "../../wallet-providers";

describe("MultisigActionProvider", () => {
let provider: MultisigActionProvider;
let mockWalletProvider: jest.Mocked<EvmWalletProvider>;

beforeEach(() => {
provider = multisigActionProvider();

mockWalletProvider = {
getAddress: jest.fn().mockReturnValue("0x1234567890123456789012345678901234567890"),
getNetwork: jest.fn().mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
chainId: "8453",
}),
sign: jest.fn().mockResolvedValue(
"0x" + "ab".repeat(65), // Mock signature
),
signMessage: jest.fn(),
signTypedData: jest.fn(),
signTransaction: jest.fn(),
sendTransaction: jest.fn(),
waitForTransactionReceipt: jest.fn(),
nativeTransfer: jest.fn(),
getBalance: jest.fn(),
getName: jest.fn().mockReturnValue("mock-wallet"),
readContract: jest.fn(),
getPublicClient: jest.fn(),
} as unknown as jest.Mocked<EvmWalletProvider>;
});

describe("signDigest", () => {
const validDigest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

it("should sign a valid digest without 0x prefix", async () => {
const result = await provider.signDigest(mockWalletProvider, { digest: validDigest });

expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validDigest}`);
expect(result).toContain("Digest signed successfully");
expect(result).toContain("Signature:");
});

it("should sign a valid digest with 0x prefix", async () => {
const result = await provider.signDigest(mockWalletProvider, {
digest: `0x${validDigest}`,
});

expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validDigest}`);
expect(result).toContain("Digest signed successfully");
});

it("should handle uppercase digest", async () => {
const result = await provider.signDigest(mockWalletProvider, {
digest: validDigest.toUpperCase(),
});

expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validDigest}`);
expect(result).toContain("Digest signed successfully");
});

it("should reject digest with wrong length", async () => {
const result = await provider.signDigest(mockWalletProvider, { digest: "abc123" });

expect(mockWalletProvider.sign).not.toHaveBeenCalled();
expect(result).toContain("Error: Digest must be exactly 32 bytes");
});

it("should handle signing errors", async () => {
mockWalletProvider.sign.mockRejectedValueOnce(new Error("Signing failed"));

const result = await provider.signDigest(mockWalletProvider, { digest: validDigest });

expect(result).toContain("Error signing digest");
});
});

describe("signSafeTransaction", () => {
const validSafeTxHash = "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd";
const safeAddress = "0xSafe1234567890123456789012345678901234";

it("should sign a valid Safe transaction hash", async () => {
const result = await provider.signSafeTransaction(mockWalletProvider, {
safeAddress,
safeTxHash: validSafeTxHash,
});

expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validSafeTxHash}`);
expect(result).toContain("Safe transaction signed successfully");
expect(result).toContain(safeAddress);
});

it("should handle 0x prefix in safeTxHash", async () => {
const result = await provider.signSafeTransaction(mockWalletProvider, {
safeAddress,
safeTxHash: `0x${validSafeTxHash}`,
});

expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validSafeTxHash}`);
expect(result).toContain("Safe transaction signed successfully");
});

it("should reject invalid safeTxHash length", async () => {
const result = await provider.signSafeTransaction(mockWalletProvider, {
safeAddress,
safeTxHash: "invalid",
});

expect(mockWalletProvider.sign).not.toHaveBeenCalled();
expect(result).toContain("Error: safeTxHash must be exactly 32 bytes");
});
});

describe("getMultisigPubkey", () => {
it("should return wallet address and network info", async () => {
const result = await provider.getMultisigPubkey(mockWalletProvider, {});

expect(result).toContain("Multisig Public Key Info");
expect(result).toContain("0x1234567890123456789012345678901234567890");
expect(result).toContain("base-mainnet");
expect(result).toContain("evm");
});
});

describe("supportsNetwork", () => {
it("should return true for EVM networks", () => {
const network = { protocolFamily: "evm", networkId: "base-mainnet", chainId: "8453" };
expect(provider.supportsNetwork(network)).toBe(true);
});

it("should return false for non-EVM networks", () => {
const network = { protocolFamily: "svm", networkId: "solana-mainnet" };
expect(provider.supportsNetwork(network)).toBe(false);
});

it("should return false for Bitcoin networks", () => {
const network = { protocolFamily: "bitcoin", networkId: "bitcoin-mainnet" };
expect(provider.supportsNetwork(network)).toBe(false);
});
});
});
Loading