Skip to content

Commit 7fa3bf9

Browse files
committed
Sync from opensea-devtools
1 parent 04e7997 commit 7fa3bf9

10 files changed

Lines changed: 553 additions & 24 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# wallet-adapters — Agent Conventions
22

3-
Provider-agnostic wallet adapter library for signing and sending transactions across managed (Privy, Turnkey, Fireblocks) and local (private key) backends. Supports both viem and ethers.js via optional bridge utilities.
3+
Provider-agnostic wallet adapter library for signing and sending transactions across managed (Bankr, Privy, Turnkey, Fireblocks) and local (private key) backends. Supports both viem and ethers.js via optional bridge utilities.
44

55
## Quick Reference
66

@@ -20,6 +20,7 @@ pnpm run type-check # TypeScript type checking
2020
|------|------|
2121
| `src/index.ts` | Library entry point — public exports |
2222
| `src/types/index.ts` | Core interfaces: `WalletAdapter`, `TransactionRequest`, `WalletCapabilities` |
23+
| `src/adapters/bankr.ts` | Bankr agent wallet API adapter |
2324
| `src/adapters/privy.ts` | Privy server-side wallet API adapter |
2425
| `src/adapters/turnkey.ts` | Turnkey HSM-backed signing with P-256 stamp auth |
2526
| `src/adapters/fireblocks.ts` | Fireblocks enterprise MPC custody adapter |
@@ -41,7 +42,7 @@ pnpm run type-check # TypeScript type checking
4142

4243
5. **TransactionRequest is extensible.** Optional fields (`gas`, `nonce`, `maxFeePerGas`, `maxPriorityFeePerGas`) let callers pass pre-estimated values to avoid redundant RPC calls.
4344

44-
6. **Environment-based construction.** Each adapter has a `fromEnv()` static factory. `createWalletFromEnv()` auto-detects with priority: Privy > Fireblocks > Turnkey > PrivateKey.
45+
6. **Environment-based construction.** Each adapter has a `fromEnv()` static factory. `createWalletFromEnv()` auto-detects with priority: Privy > Fireblocks > Turnkey > Bankr > PrivateKey.
4546

4647
## Review Checklist
4748

@@ -53,7 +54,7 @@ When reviewing changes to this package, verify:
5354

5455
3. **Security of key material.** Private keys, API secrets, and signing keys must never be logged, included in error messages, or exposed in stack traces.
5556

56-
4. **Cryptographic correctness.** The Turnkey adapter uses P-256 ECDSA with DER-encoded signatures. The Fireblocks adapter uses RS256 (RSASSA-PKCS1-v1_5 with SHA-256) for JWT signing. The private-key adapter uses `@noble/curves/secp256k1` for ECDSA signing.
57+
4. **Cryptographic correctness.** The Turnkey adapter uses P-256 ECDSA with DER-encoded signatures. The Fireblocks adapter uses RS256 (RSASSA-PKCS1-v1_5 with SHA-256) for JWT signing. The private-key adapter uses `@noble/curves/secp256k1` for ECDSA signing. The Bankr adapter delegates signing to the Bankr Wallet API.
5758

5859
5. **Bridge isolation.** viem and ethers bridges must remain in separate entry points (`./viem`, `./ethers`). They must not be imported by the core adapters.
5960

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"opensea",
6262
"wallet",
6363
"adapter",
64+
"bankr",
6465
"privy",
6566
"turnkey",
6667
"fireblocks",

src/__tests__/adapters.test.ts

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type MockInstance,
88
vi,
99
} from "vitest"
10+
import { BankrAdapter } from "../adapters/bankr.js"
1011
import { FireblocksAdapter } from "../adapters/fireblocks.js"
1112
import { PrivateKeyAdapter } from "../adapters/private-key.js"
1213
import { PrivyAdapter } from "../adapters/privy.js"
@@ -464,3 +465,284 @@ describe("FireblocksAdapter signMessage/signTypedData", () => {
464465
).toBeTruthy()
465466
})
466467
})
468+
469+
describe("BankrAdapter", () => {
470+
it("constructs with valid config", () => {
471+
const adapter = new BankrAdapter({ apiKey: "test-api-key" })
472+
expect(adapter.name).toBe("bankr")
473+
expect(adapter.capabilities.signMessage).toBe(true)
474+
expect(adapter.capabilities.signTypedData).toBe(true)
475+
expect(adapter.capabilities.managedGas).toBe(true)
476+
expect(adapter.capabilities.managedNonce).toBe(true)
477+
})
478+
479+
it("fromEnv throws when BANKR_API_KEY is missing", () => {
480+
expect(() => BankrAdapter.fromEnv()).toThrow(
481+
"BANKR_API_KEY environment variable is required",
482+
)
483+
})
484+
485+
it("fromEnv creates adapter when env vars are set", () => {
486+
process.env.BANKR_API_KEY = "test-api-key"
487+
const adapter = BankrAdapter.fromEnv()
488+
expect(adapter.name).toBe("bankr")
489+
delete process.env.BANKR_API_KEY
490+
})
491+
})
492+
493+
describe("BankrAdapter getAddress", () => {
494+
let fetchSpy: FetchMock
495+
496+
beforeEach(() => {
497+
fetchSpy = vi.spyOn(global, "fetch")
498+
})
499+
500+
afterEach(() => {
501+
fetchSpy.mockRestore()
502+
})
503+
504+
it("fetches EVM address from /wallet/me", async () => {
505+
const adapter = new BankrAdapter({ apiKey: "test-key" })
506+
507+
fetchSpy.mockResolvedValueOnce(
508+
new Response(
509+
JSON.stringify({
510+
wallets: [
511+
{
512+
chain: "evm",
513+
address: "0x1234567890abcdef1234567890abcdef12345678",
514+
},
515+
{ chain: "solana", address: "5DcK...NdR" },
516+
],
517+
}),
518+
{ status: 200 },
519+
),
520+
)
521+
522+
const address = await adapter.getAddress()
523+
expect(address).toBe("0x1234567890abcdef1234567890abcdef12345678")
524+
525+
expect(fetchSpy).toHaveBeenCalledWith(
526+
"https://api.bankr.bot/wallet/me",
527+
expect.objectContaining({
528+
headers: expect.objectContaining({ "X-API-Key": "test-key" }),
529+
}),
530+
)
531+
})
532+
533+
it("caches address after first fetch", async () => {
534+
const adapter = new BankrAdapter({ apiKey: "test-key" })
535+
536+
fetchSpy.mockResolvedValueOnce(
537+
new Response(
538+
JSON.stringify({
539+
wallets: [{ chain: "evm", address: "0xaabbccdd" }],
540+
}),
541+
{ status: 200 },
542+
),
543+
)
544+
545+
await adapter.getAddress()
546+
const address = await adapter.getAddress()
547+
expect(address).toBe("0xaabbccdd")
548+
expect(fetchSpy).toHaveBeenCalledTimes(1)
549+
})
550+
551+
it("throws when no EVM wallet is found", async () => {
552+
const adapter = new BankrAdapter({ apiKey: "test-key" })
553+
554+
fetchSpy.mockResolvedValueOnce(
555+
new Response(
556+
JSON.stringify({
557+
wallets: [{ chain: "solana", address: "5DcK...NdR" }],
558+
}),
559+
{ status: 200 },
560+
),
561+
)
562+
563+
await expect(adapter.getAddress()).rejects.toThrow(
564+
"Bankr wallet has no EVM address",
565+
)
566+
})
567+
568+
it("throws on API error", async () => {
569+
const adapter = new BankrAdapter({ apiKey: "bad-key" })
570+
571+
fetchSpy.mockResolvedValueOnce(
572+
new Response("Unauthorized", { status: 401 }),
573+
)
574+
575+
await expect(adapter.getAddress()).rejects.toThrow(
576+
"Bankr getAddress failed (401)",
577+
)
578+
})
579+
})
580+
581+
describe("BankrAdapter sendTransaction", () => {
582+
let fetchSpy: FetchMock
583+
584+
beforeEach(() => {
585+
fetchSpy = vi.spyOn(global, "fetch")
586+
})
587+
588+
afterEach(() => {
589+
fetchSpy.mockRestore()
590+
})
591+
592+
it("submits transaction via /wallet/submit", async () => {
593+
const adapter = new BankrAdapter({ apiKey: "test-key" })
594+
595+
fetchSpy.mockResolvedValueOnce(
596+
new Response(
597+
JSON.stringify({
598+
transactionHash: "0xdeadbeef",
599+
status: "success",
600+
}),
601+
{ status: 200 },
602+
),
603+
)
604+
605+
const result = await adapter.sendTransaction({
606+
to: "0x1111111111111111111111111111111111111111",
607+
data: "0x",
608+
value: "1000000000000000000",
609+
chainId: 8453,
610+
})
611+
expect(result.hash).toBe("0xdeadbeef")
612+
613+
const reqInit = fetchSpy.mock.calls[0][1] as RequestInit
614+
const body = JSON.parse(reqInit.body as string)
615+
expect(body.transaction.to).toBe(
616+
"0x1111111111111111111111111111111111111111",
617+
)
618+
expect(body.transaction.chainId).toBe(8453)
619+
expect(body.transaction.data).toBe("0x")
620+
expect(body.transaction.value).toBe("1000000000000000000")
621+
expect(body.waitForConfirmation).toBe(true)
622+
})
623+
624+
it("includes data and value even when zero", async () => {
625+
const adapter = new BankrAdapter({ apiKey: "test-key" })
626+
627+
fetchSpy.mockResolvedValueOnce(
628+
new Response(JSON.stringify({ transactionHash: "0xabc" }), {
629+
status: 200,
630+
}),
631+
)
632+
633+
await adapter.sendTransaction({
634+
to: "0x1111111111111111111111111111111111111111",
635+
data: "0x",
636+
value: "0",
637+
chainId: 1,
638+
})
639+
640+
const reqInit = fetchSpy.mock.calls[0][1] as RequestInit
641+
const body = JSON.parse(reqInit.body as string)
642+
expect(body.transaction.data).toBe("0x")
643+
expect(body.transaction.value).toBe("0")
644+
})
645+
646+
it("throws on API error", async () => {
647+
const adapter = new BankrAdapter({ apiKey: "test-key" })
648+
649+
fetchSpy.mockResolvedValueOnce(new Response("Forbidden", { status: 403 }))
650+
651+
await expect(
652+
adapter.sendTransaction({
653+
to: "0x1111111111111111111111111111111111111111",
654+
data: "0x",
655+
value: "0",
656+
chainId: 1,
657+
}),
658+
).rejects.toThrow("Bankr sendTransaction failed (403)")
659+
})
660+
})
661+
662+
describe("BankrAdapter signMessage/signTypedData", () => {
663+
let fetchSpy: FetchMock
664+
665+
beforeEach(() => {
666+
fetchSpy = vi.spyOn(global, "fetch")
667+
})
668+
669+
afterEach(() => {
670+
fetchSpy.mockRestore()
671+
})
672+
673+
it("signMessage calls personal_sign via /wallet/sign", async () => {
674+
const adapter = new BankrAdapter({ apiKey: "test-key" })
675+
676+
fetchSpy.mockResolvedValueOnce(
677+
new Response(JSON.stringify({ signature: `0x${"ab".repeat(65)}` }), {
678+
status: 200,
679+
}),
680+
)
681+
682+
const sig = await adapter.signMessage({ message: "hello" })
683+
expect(sig).toBe(`0x${"ab".repeat(65)}`)
684+
685+
expect(fetchSpy).toHaveBeenCalledWith(
686+
"https://api.bankr.bot/wallet/sign",
687+
expect.objectContaining({
688+
method: "POST",
689+
}),
690+
)
691+
692+
const reqInit = fetchSpy.mock.calls[0][1] as RequestInit
693+
const body = JSON.parse(reqInit.body as string)
694+
expect(body.signatureType).toBe("personal_sign")
695+
expect(body.message).toBe("hello")
696+
})
697+
698+
it("signTypedData calls eth_signTypedData_v4 via /wallet/sign", async () => {
699+
const adapter = new BankrAdapter({ apiKey: "test-key" })
700+
701+
fetchSpy.mockResolvedValueOnce(
702+
new Response(JSON.stringify({ signature: `0x${"cd".repeat(65)}` }), {
703+
status: 200,
704+
}),
705+
)
706+
707+
const sig = await adapter.signTypedData({
708+
domain: { name: "Test", version: "1", chainId: 1 },
709+
types: { Message: [{ name: "content", type: "string" }] },
710+
primaryType: "Message",
711+
message: { content: "hello" },
712+
})
713+
expect(sig).toBe(`0x${"cd".repeat(65)}`)
714+
715+
const reqInit = fetchSpy.mock.calls[0][1] as RequestInit
716+
const body = JSON.parse(reqInit.body as string)
717+
expect(body.signatureType).toBe("eth_signTypedData_v4")
718+
expect(body.typedData.primaryType).toBe("Message")
719+
expect(body.typedData.domain.name).toBe("Test")
720+
})
721+
722+
it("signMessage throws on API error", async () => {
723+
const adapter = new BankrAdapter({ apiKey: "test-key" })
724+
725+
fetchSpy.mockResolvedValueOnce(
726+
new Response("Rate limited", { status: 429 }),
727+
)
728+
729+
await expect(adapter.signMessage({ message: "hello" })).rejects.toThrow(
730+
"Bankr signMessage failed (429)",
731+
)
732+
})
733+
734+
it("signTypedData throws on API error", async () => {
735+
const adapter = new BankrAdapter({ apiKey: "test-key" })
736+
737+
fetchSpy.mockResolvedValueOnce(new Response("Forbidden", { status: 403 }))
738+
739+
await expect(
740+
adapter.signTypedData({
741+
domain: { name: "Test" },
742+
types: { Message: [{ name: "content", type: "string" }] },
743+
primaryType: "Message",
744+
message: { content: "hello" },
745+
}),
746+
).rejects.toThrow("Bankr signTypedData failed (403)")
747+
})
748+
})

0 commit comments

Comments
 (0)