Skip to content

Commit c8a484e

Browse files
committed
feat: add first-class bankr signer backend
1 parent 06e8f1d commit c8a484e

9 files changed

Lines changed: 393 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Added
6+
7+
- Native `bankr` signer backend:
8+
- signer spec: `bankr[:address|apiKeyEnv|apiUrl]`
9+
- default auth env var: `BANKR_API_KEY`
10+
- default API URL: `https://api.bankr.bot`
11+
- address auto-resolution via `GET /agent/me` when address is not pinned
12+
- transaction submit via `POST /agent/submit`
13+
- Bootstrap overrides now support `bankr`:
14+
- `--signer-address` to pin wallet address
15+
- `--signer-auth-env-var` to customize Bankr API key env var
16+
317
## 0.2.1 - 2026-02-27
418

519
### Added

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,26 @@ npm run ag -- auction active --first 5 --raw --json
147147
- `keychain:ACCOUNT_ID` (encrypted local key store; requires `AGCLI_KEYCHAIN_PASSPHRASE`)
148148
- `remote:URL|ADDRESS|AUTH_ENV` (HTTP signer service)
149149
- `ledger:DERIVATION_PATH|ADDRESS|BRIDGE_ENV` (external bridge command signer)
150+
- `bankr[:ADDRESS|API_KEY_ENV|API_URL]` (Bankr-native signer via `/agent/me` + `/agent/submit`; defaults: `BANKR_API_KEY`, `https://api.bankr.bot`)
150151

151152
Remote signer contract:
152153

153154
- `GET /address` -> `{ "address": "0x..." }` (optional if address configured)
154155
- `POST /sign-transaction` -> `{ "rawTransaction": "0x..." }` or `{ "txHash": "0x..." }`
155156

157+
Bankr signer contract:
158+
159+
- `GET /agent/me` -> resolves wallet address when signer address is not pinned
160+
- `POST /agent/submit` -> submits transaction and returns transaction hash
161+
- auth header: `x-api-key: <BANKR_API_KEY>`
162+
163+
Bankr bootstrap example:
164+
165+
```bash
166+
BANKR_API_KEY=... \
167+
npm run ag -- bootstrap --mode agent --profile bankr --chain base --signer bankr --json
168+
```
169+
156170
Ledger bridge contract:
157171

158172
- Set `AGCLI_LEDGER_BRIDGE_CMD` (or custom env var in signer config) to a command that reads tx payload JSON from stdin and outputs JSON containing either `rawTransaction` or `txHash`.

src/commands/bootstrap.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ function buildSignerConfig(ctx: CommandContext, signerInput: string): ProfileCon
3939
};
4040
}
4141

42+
if (signer.type === "bankr") {
43+
const address = getFlagString(ctx.args.flags, "signer-address");
44+
const apiKeyEnvVar = getFlagString(ctx.args.flags, "signer-auth-env-var");
45+
46+
return {
47+
...signer,
48+
...(address ? { address: address as `0x${string}` } : {}),
49+
...(apiKeyEnvVar ? { apiKeyEnvVar } : {}),
50+
};
51+
}
52+
4253
if (signer.type === "ledger") {
4354
const address = getFlagString(ctx.args.flags, "signer-address");
4455
const bridgeCommandEnvVar = getFlagString(ctx.args.flags, "signer-bridge-env-var");

src/output.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ Bootstrap flags:
112112
--profile NAME Profile to create or update (required)
113113
--chain base|base-sepolia|<id> Chain key or numeric chain id (default: base)
114114
--rpc-url URL RPC endpoint (optional when chain preset exists)
115-
--signer readonly|env:VAR|keychain:<id>|ledger[:path|address|bridgeEnv]|remote:<url|address|authEnv>
116-
--signer-address 0x... Optional override for remote/ledger signer address
117-
--signer-auth-env-var ENV_VAR Optional remote signer bearer token env var
115+
--signer readonly|env:VAR|keychain:<id>|ledger[:path|address|bridgeEnv]|remote:<url|address|authEnv>|bankr[:address|apiKeyEnv|apiUrl]
116+
--signer-address 0x... Optional override for remote/ledger/bankr signer address
117+
--signer-auth-env-var ENV_VAR Optional remote bearer token or bankr API key env var
118118
--signer-bridge-env-var ENV_VAR Optional ledger bridge command env var name
119119
--policy NAME Policy label (default: default)
120120
--skip-signer-check Persist signer config without backend validation
@@ -124,6 +124,7 @@ Examples:
124124
AGCLI_KEYCHAIN_PASSPHRASE=... AGCLI_PRIVATE_KEY=0x... ag signer keychain import --account-id bot --private-key-env AGCLI_PRIVATE_KEY --json
125125
ag tx send --profile prod --to 0xabc... --value-wei 1000000000000000 --wait --json
126126
ag tx send --profile prod --to 0xabc... --value-wei 0 --dry-run --json
127+
BANKR_API_KEY=... ag bootstrap --mode agent --profile bankr --chain base --signer bankr --json
127128
ag subgraph check --source core-base --json
128129
ag baazaar listing active --kind erc721 --first 20 --json
129130
ag auction active --first 20 --json

src/schemas.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export const signerSchema = z.discriminatedUnion("type", [
2121
address: addressSchema.optional(),
2222
authEnvVar: z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional(),
2323
}),
24+
z.object({
25+
type: z.literal("bankr"),
26+
address: addressSchema.optional(),
27+
apiKeyEnvVar: z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional(),
28+
apiUrl: z.string().url().optional(),
29+
}),
2430
]);
2531

2632
export const policySchema = z.object({

src/signer-runtime.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const tempFiles: string[] = [];
2020
afterEach(() => {
2121
delete process.env.AGCLI_REMOTE_TOKEN;
2222
delete process.env.AGCLI_LEDGER_BRIDGE_CMD;
23+
delete process.env.BANKR_API_KEY;
24+
delete process.env.BANKR_TEST_KEY;
2325

2426
for (const file of tempFiles.splice(0)) {
2527
fs.rmSync(file, { force: true });
@@ -134,4 +136,79 @@ describe("signer runtime", () => {
134136

135137
expect(hash).toBe("0x" + "c".repeat(64));
136138
});
139+
140+
it("supports bankr signer with wallet auto-discovery", async () => {
141+
process.env.BANKR_API_KEY = "bankr-token";
142+
143+
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
144+
if (url.endsWith("/agent/me")) {
145+
expect((init?.headers as Record<string, string>)["x-api-key"]).toBe("bankr-token");
146+
return {
147+
ok: true,
148+
text: async () => JSON.stringify({ walletAddress: "0x0000000000000000000000000000000000000001" }),
149+
};
150+
}
151+
152+
expect(url.endsWith("/agent/submit")).toBe(true);
153+
expect((init?.headers as Record<string, string>)["x-api-key"]).toBe("bankr-token");
154+
const body = JSON.parse(String(init?.body));
155+
expect(body.waitForConfirmation).toBe(false);
156+
expect(body.transaction.to).toBe("0x0000000000000000000000000000000000000001");
157+
return {
158+
ok: true,
159+
text: async () => JSON.stringify({ transactionHash: "0x" + "e".repeat(64) }),
160+
};
161+
});
162+
vi.stubGlobal("fetch", fetchMock);
163+
164+
const signer = parseSigner("bankr");
165+
const runtime = await resolveSignerRuntime(signer, fakePublicClient(), "https://mainnet.base.org", base);
166+
167+
expect(runtime.summary.signerType).toBe("bankr");
168+
expect(runtime.summary.address).toBe("0x0000000000000000000000000000000000000001");
169+
expect(runtime.summary.canSign).toBe(true);
170+
171+
const hash = await runtime.sendTransaction!({
172+
chain: base,
173+
to: "0x0000000000000000000000000000000000000001",
174+
data: "0x",
175+
value: 0n,
176+
gas: 21000n,
177+
nonce: 1,
178+
});
179+
180+
expect(hash).toBe("0x" + "e".repeat(64));
181+
expect(fetchMock).toHaveBeenCalledTimes(2);
182+
});
183+
184+
it("supports bankr signer with explicit address and custom api key env var", async () => {
185+
process.env.BANKR_TEST_KEY = "bankr-token";
186+
187+
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
188+
expect(url).toBe("https://api.bankr.bot/agent/submit");
189+
expect((init?.headers as Record<string, string>)["x-api-key"]).toBe("bankr-token");
190+
return {
191+
ok: true,
192+
text: async () => JSON.stringify({ txHash: "0x" + "f".repeat(64) }),
193+
};
194+
});
195+
vi.stubGlobal("fetch", fetchMock);
196+
197+
const signer = parseSigner(
198+
"bankr:0x0000000000000000000000000000000000000001|BANKR_TEST_KEY|https://api.bankr.bot",
199+
);
200+
const runtime = await resolveSignerRuntime(signer, fakePublicClient(), "https://mainnet.base.org", base);
201+
202+
const hash = await runtime.sendTransaction!({
203+
chain: base,
204+
to: "0x0000000000000000000000000000000000000001",
205+
data: "0x",
206+
value: 0n,
207+
gas: 21000n,
208+
nonce: 1,
209+
});
210+
211+
expect(hash).toBe("0x" + "f".repeat(64));
212+
expect(fetchMock).toHaveBeenCalledTimes(1);
213+
});
137214
});

src/signer.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@ describe("agcli signer parser", () => {
5757
});
5858
});
5959

60+
it("parses bankr signer defaults", () => {
61+
expect(parseSigner("bankr")).toEqual({
62+
type: "bankr",
63+
});
64+
});
65+
66+
it("parses bankr signer with address, api env, and api url", () => {
67+
expect(
68+
parseSigner("bankr:0x0000000000000000000000000000000000000001|BANKR_KEY|https://api.bankr.bot"),
69+
).toEqual({
70+
type: "bankr",
71+
address: "0x0000000000000000000000000000000000000001",
72+
apiKeyEnvVar: "BANKR_KEY",
73+
apiUrl: "https://api.bankr.bot",
74+
});
75+
});
76+
6077
it("throws for malformed env name", () => {
6178
expect(() => parseSigner("env:agcli_key")).toThrowError();
6279
});

0 commit comments

Comments
 (0)