agent-operator 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +0 -0
- package/dist/config.d.ts +8 -0
- package/dist/connectors/base.d.ts +7 -1
- package/dist/connectors/base.js +64 -28
- package/dist/connectors/solana.d.ts +7 -1
- package/dist/connectors/solana.js +66 -18
- package/dist/connectors/tempo.d.ts +8 -2
- package/dist/connectors/tempo.js +59 -25
- package/dist/connectors/types.d.ts +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/operator.d.ts +1 -1
- package/dist/operator.js +35 -23
- package/dist/policies/low-balance.js +15 -13
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { PoliciesConfig } from "./policies/types.js";
|
|
2
2
|
import type { AlertsConfig } from "./notifications/types.js";
|
|
3
|
+
export type Network = "mainnet" | "testnet";
|
|
4
|
+
export interface TokenConfig {
|
|
5
|
+
address: string;
|
|
6
|
+
symbol: string;
|
|
7
|
+
decimals?: number;
|
|
8
|
+
}
|
|
3
9
|
export interface WalletConfig {
|
|
4
10
|
walletId: string;
|
|
5
11
|
chains: string[];
|
|
12
|
+
network?: Network;
|
|
13
|
+
tokens?: Record<string, TokenConfig[]>;
|
|
6
14
|
policies: PoliciesConfig;
|
|
7
15
|
notifications: NotificationConfig;
|
|
8
16
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import type { Network, TokenConfig } from "../config.js";
|
|
1
2
|
import type { Connector, Session, SessionResult, Balance } from "./types.js";
|
|
2
3
|
export declare class BaseConnector implements Connector {
|
|
3
4
|
chain: string;
|
|
5
|
+
network: Network;
|
|
6
|
+
tokens: TokenConfig[];
|
|
7
|
+
constructor(network?: Network, tokens?: TokenConfig[]);
|
|
8
|
+
private get chainId();
|
|
9
|
+
private getViemChain;
|
|
4
10
|
private sessions;
|
|
5
11
|
openSession(params: {
|
|
6
12
|
service: string;
|
|
@@ -8,5 +14,5 @@ export declare class BaseConnector implements Connector {
|
|
|
8
14
|
walletId: string;
|
|
9
15
|
}): Promise<Session>;
|
|
10
16
|
closeSession(sessionId: string): Promise<SessionResult>;
|
|
11
|
-
|
|
17
|
+
getBalances(walletId: string): Promise<Balance[]>;
|
|
12
18
|
}
|
package/dist/connectors/base.js
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
import { Mppx } from "mppx/client";
|
|
2
2
|
import { Method, Credential } from "mppx";
|
|
3
3
|
import { signTransaction, getWallet } from "@open-wallet-standard/core";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const DEFAULT_TOKENS = {
|
|
5
|
+
mainnet: [
|
|
6
|
+
{ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol: "USDC", decimals: 6 },
|
|
7
|
+
],
|
|
8
|
+
testnet: [
|
|
9
|
+
{ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", symbol: "USDC", decimals: 6 },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
const CHAIN_IDS = {
|
|
13
|
+
mainnet: "eip155:8453",
|
|
14
|
+
testnet: "eip155:84532",
|
|
15
|
+
};
|
|
6
16
|
export class BaseConnector {
|
|
7
17
|
chain = "base";
|
|
18
|
+
network;
|
|
19
|
+
tokens;
|
|
20
|
+
constructor(network = "mainnet", tokens) {
|
|
21
|
+
this.network = network;
|
|
22
|
+
this.tokens = tokens ?? DEFAULT_TOKENS[network];
|
|
23
|
+
}
|
|
24
|
+
get chainId() {
|
|
25
|
+
return CHAIN_IDS[this.network];
|
|
26
|
+
}
|
|
27
|
+
async getViemChain() {
|
|
28
|
+
const chains = await import("viem/chains");
|
|
29
|
+
return this.network === "testnet" ? chains.baseSepolia : chains.base;
|
|
30
|
+
}
|
|
8
31
|
sessions = new Map();
|
|
9
32
|
async openSession(params) {
|
|
10
33
|
const { service, maxBudget, walletId } = params;
|
|
34
|
+
const chainId = this.chainId;
|
|
11
35
|
const wallet = getWallet(walletId);
|
|
12
36
|
const baseAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
|
|
13
37
|
if (!baseAccount) {
|
|
14
38
|
throw new Error(`Wallet '${walletId}' has no EVM account for Base`);
|
|
15
39
|
}
|
|
16
|
-
// define a custom charge method for Base
|
|
17
40
|
const baseChargeMethod = Method.from({
|
|
18
41
|
name: "base",
|
|
19
42
|
intent: "charge",
|
|
@@ -32,26 +55,24 @@ export class BaseConnector {
|
|
|
32
55
|
});
|
|
33
56
|
const clientMethod = Method.toClient(baseChargeMethod, {
|
|
34
57
|
async createCredential({ challenge }) {
|
|
35
|
-
// sign the payment via OWS (policies enforced here)
|
|
36
58
|
const txData = JSON.stringify({
|
|
37
59
|
challengeId: challenge.id,
|
|
38
60
|
from: baseAccount.address,
|
|
39
61
|
amount: challenge.request?.amount,
|
|
40
62
|
chain: "base",
|
|
41
63
|
});
|
|
42
|
-
const result = signTransaction(walletId,
|
|
64
|
+
const result = signTransaction(walletId, chainId, txData);
|
|
43
65
|
return Credential.serialize({
|
|
44
66
|
challenge,
|
|
45
67
|
payload: { signature: result.signature },
|
|
46
|
-
source: `did:pkh
|
|
68
|
+
source: `did:pkh:${chainId}:${baseAccount.address}`,
|
|
47
69
|
});
|
|
48
70
|
},
|
|
49
71
|
});
|
|
50
72
|
const mppxClient = Mppx.create({
|
|
51
73
|
methods: [clientMethod],
|
|
52
74
|
});
|
|
53
|
-
|
|
54
|
-
const response = await mppxClient.fetch(service, {
|
|
75
|
+
await mppxClient.fetch(service, {
|
|
55
76
|
headers: { "X-Max-Budget": String(maxBudget) },
|
|
56
77
|
});
|
|
57
78
|
const sessionId = crypto.randomUUID();
|
|
@@ -83,37 +104,52 @@ export class BaseConnector {
|
|
|
83
104
|
refund: session.maxBudget - session.spent,
|
|
84
105
|
};
|
|
85
106
|
}
|
|
86
|
-
async
|
|
107
|
+
async getBalances(walletId) {
|
|
87
108
|
const wallet = getWallet(walletId);
|
|
88
109
|
const baseAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
|
|
89
110
|
if (!baseAccount) {
|
|
90
|
-
return
|
|
111
|
+
return this.tokens.map((t) => ({
|
|
112
|
+
chain: this.chain,
|
|
113
|
+
amount: 0,
|
|
114
|
+
currency: t.symbol,
|
|
115
|
+
}));
|
|
91
116
|
}
|
|
92
117
|
try {
|
|
93
118
|
const { createPublicClient, http, parseAbi, formatUnits } = await import("viem");
|
|
94
|
-
const
|
|
119
|
+
const chain = await this.getViemChain();
|
|
95
120
|
const client = createPublicClient({
|
|
96
|
-
chain
|
|
121
|
+
chain,
|
|
97
122
|
transport: http(),
|
|
98
123
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
const balanceAbi = parseAbi([
|
|
125
|
+
"function balanceOf(address) view returns (uint256)",
|
|
126
|
+
]);
|
|
127
|
+
const results = await Promise.all(this.tokens.map(async (token) => {
|
|
128
|
+
try {
|
|
129
|
+
const balance = await client.readContract({
|
|
130
|
+
address: token.address,
|
|
131
|
+
abi: balanceAbi,
|
|
132
|
+
functionName: "balanceOf",
|
|
133
|
+
args: [baseAccount.address],
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
chain: this.chain,
|
|
137
|
+
amount: Number(formatUnits(balance, token.decimals ?? 6)),
|
|
138
|
+
currency: token.symbol,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { chain: this.chain, amount: 0, currency: token.symbol };
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
return results;
|
|
114
146
|
}
|
|
115
147
|
catch {
|
|
116
|
-
return
|
|
148
|
+
return this.tokens.map((t) => ({
|
|
149
|
+
chain: this.chain,
|
|
150
|
+
amount: 0,
|
|
151
|
+
currency: t.symbol,
|
|
152
|
+
}));
|
|
117
153
|
}
|
|
118
154
|
}
|
|
119
155
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import type { Network, TokenConfig } from "../config.js";
|
|
1
2
|
import type { Connector, Session, SessionResult, Balance } from "./types.js";
|
|
2
3
|
export declare class SolanaConnector implements Connector {
|
|
3
4
|
chain: string;
|
|
5
|
+
network: Network;
|
|
6
|
+
tokens: TokenConfig[];
|
|
4
7
|
private sessions;
|
|
8
|
+
constructor(network?: Network, tokens?: TokenConfig[]);
|
|
9
|
+
private get rpcUrl();
|
|
10
|
+
private get solanaChainId();
|
|
5
11
|
openSession(params: {
|
|
6
12
|
service: string;
|
|
7
13
|
maxBudget: number;
|
|
8
14
|
walletId: string;
|
|
9
15
|
}): Promise<Session>;
|
|
10
16
|
closeSession(sessionId: string): Promise<SessionResult>;
|
|
11
|
-
|
|
17
|
+
getBalances(walletId: string): Promise<Balance[]>;
|
|
12
18
|
}
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { getWallet, signTransaction } from "@open-wallet-standard/core";
|
|
2
2
|
import { solana } from "@solana/mpp/client";
|
|
3
3
|
import { Mppx } from "mppx/client";
|
|
4
|
+
const RPC_URLS = {
|
|
5
|
+
mainnet: "https://api.mainnet-beta.solana.com",
|
|
6
|
+
testnet: "https://api.devnet.solana.com",
|
|
7
|
+
};
|
|
8
|
+
const SOLANA_CHAIN_IDS = {
|
|
9
|
+
mainnet: "solana:mainnet-beta",
|
|
10
|
+
testnet: "solana:devnet",
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_TOKENS = [
|
|
13
|
+
{ address: "native", symbol: "SOL", decimals: 9 },
|
|
14
|
+
];
|
|
4
15
|
export class SolanaConnector {
|
|
5
16
|
chain = "solana";
|
|
17
|
+
network;
|
|
18
|
+
tokens;
|
|
6
19
|
sessions = new Map();
|
|
20
|
+
constructor(network = "mainnet", tokens) {
|
|
21
|
+
this.network = network;
|
|
22
|
+
this.tokens = tokens ?? DEFAULT_TOKENS;
|
|
23
|
+
}
|
|
24
|
+
get rpcUrl() {
|
|
25
|
+
return RPC_URLS[this.network];
|
|
26
|
+
}
|
|
27
|
+
get solanaChainId() {
|
|
28
|
+
return SOLANA_CHAIN_IDS[this.network];
|
|
29
|
+
}
|
|
7
30
|
async openSession(params) {
|
|
8
31
|
const { service, maxBudget, walletId } = params;
|
|
9
32
|
const wallet = getWallet(walletId);
|
|
@@ -11,8 +34,8 @@ export class SolanaConnector {
|
|
|
11
34
|
if (!solanaAccount) {
|
|
12
35
|
throw new Error(`Wallet '${walletId}' has no Solana account`);
|
|
13
36
|
}
|
|
14
|
-
// create an OWS-backed TransactionSigner compatible with @solana/kit
|
|
15
37
|
const { address } = await import("@solana/kit");
|
|
38
|
+
const solanaChainId = this.solanaChainId;
|
|
16
39
|
const owsSigner = {
|
|
17
40
|
address: address(solanaAccount.address),
|
|
18
41
|
async signTransactions(transactions) {
|
|
@@ -21,20 +44,18 @@ export class SolanaConnector {
|
|
|
21
44
|
? tx.serialize()
|
|
22
45
|
: new Uint8Array(tx);
|
|
23
46
|
const txHex = Buffer.from(txBytes).toString("hex");
|
|
24
|
-
const result = signTransaction(walletId,
|
|
25
|
-
// return the signed transaction bytes
|
|
47
|
+
const result = signTransaction(walletId, solanaChainId, txHex);
|
|
26
48
|
return Buffer.from(result.signature, "hex");
|
|
27
49
|
}));
|
|
28
50
|
},
|
|
29
51
|
};
|
|
30
|
-
// create @solana/mpp charge method with OWS signer
|
|
31
52
|
const chargeMethod = solana.charge({
|
|
32
53
|
signer: owsSigner,
|
|
54
|
+
rpcUrl: this.rpcUrl,
|
|
33
55
|
});
|
|
34
56
|
const mppxClient = Mppx.create({
|
|
35
57
|
methods: [chargeMethod],
|
|
36
58
|
});
|
|
37
|
-
// initiate payment via the 402 challenge-response flow
|
|
38
59
|
await mppxClient.fetch(service, {
|
|
39
60
|
headers: { "X-Max-Budget": String(maxBudget) },
|
|
40
61
|
});
|
|
@@ -65,27 +86,54 @@ export class SolanaConnector {
|
|
|
65
86
|
totalSpent: session.maxBudget,
|
|
66
87
|
};
|
|
67
88
|
}
|
|
68
|
-
async
|
|
89
|
+
async getBalances(walletId) {
|
|
69
90
|
const wallet = getWallet(walletId);
|
|
70
91
|
const solanaAccount = wallet.accounts.find((a) => a.chainId.startsWith("solana"));
|
|
71
92
|
if (!solanaAccount) {
|
|
72
|
-
return
|
|
93
|
+
return this.tokens.map((t) => ({
|
|
94
|
+
chain: this.chain,
|
|
95
|
+
amount: 0,
|
|
96
|
+
currency: t.symbol,
|
|
97
|
+
}));
|
|
73
98
|
}
|
|
74
99
|
try {
|
|
75
100
|
const { createSolanaRpc } = await import("@solana/kit");
|
|
76
|
-
const rpc = createSolanaRpc(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
const rpc = createSolanaRpc(this.rpcUrl);
|
|
102
|
+
const results = await Promise.all(this.tokens.map(async (token) => {
|
|
103
|
+
try {
|
|
104
|
+
if (token.address === "native") {
|
|
105
|
+
const { value } = await rpc
|
|
106
|
+
.getBalance(solanaAccount.address)
|
|
107
|
+
.send();
|
|
108
|
+
return {
|
|
109
|
+
chain: this.chain,
|
|
110
|
+
amount: Number(value) / 1e9,
|
|
111
|
+
currency: token.symbol,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// SPL token balance
|
|
115
|
+
const { value } = await rpc
|
|
116
|
+
.getTokenAccountsByOwner(solanaAccount.address, { mint: token.address }, { encoding: "jsonParsed" })
|
|
117
|
+
.send();
|
|
118
|
+
const amount = value?.[0]?.account?.data?.parsed?.info?.tokenAmount?.uiAmount ?? 0;
|
|
119
|
+
return {
|
|
120
|
+
chain: this.chain,
|
|
121
|
+
amount,
|
|
122
|
+
currency: token.symbol,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { chain: this.chain, amount: 0, currency: token.symbol };
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
return results;
|
|
86
130
|
}
|
|
87
131
|
catch {
|
|
88
|
-
return
|
|
132
|
+
return this.tokens.map((t) => ({
|
|
133
|
+
chain: this.chain,
|
|
134
|
+
amount: 0,
|
|
135
|
+
currency: t.symbol,
|
|
136
|
+
}));
|
|
89
137
|
}
|
|
90
138
|
}
|
|
91
139
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import type { Network, TokenConfig } from "../config.js";
|
|
1
2
|
import type { Connector, Session, SessionResult, Balance } from "./types.js";
|
|
2
3
|
export declare class TempoConnector implements Connector {
|
|
3
4
|
chain: string;
|
|
4
|
-
|
|
5
|
+
network: Network;
|
|
6
|
+
tokens: TokenConfig[];
|
|
7
|
+
constructor(network?: Network, tokens?: TokenConfig[]);
|
|
8
|
+
private getChain;
|
|
9
|
+
private getChainId;
|
|
5
10
|
openSession(params: {
|
|
6
11
|
service: string;
|
|
7
12
|
maxBudget: number;
|
|
8
13
|
walletId: string;
|
|
9
14
|
}): Promise<Session>;
|
|
15
|
+
private sessions;
|
|
10
16
|
closeSession(sessionId: string): Promise<SessionResult>;
|
|
11
|
-
|
|
17
|
+
getBalances(walletId: string): Promise<Balance[]>;
|
|
12
18
|
}
|
package/dist/connectors/tempo.js
CHANGED
|
@@ -1,46 +1,58 @@
|
|
|
1
1
|
import { tempo as tempoClient } from "mppx/client";
|
|
2
2
|
import { signMessage, getWallet } from "@open-wallet-standard/core";
|
|
3
|
+
const DEFAULT_TOKENS = [
|
|
4
|
+
{ address: "0x20c0000000000000000000000000000000000000", symbol: "pathUSD", decimals: 6 },
|
|
5
|
+
];
|
|
3
6
|
export class TempoConnector {
|
|
4
7
|
chain = "tempo";
|
|
5
|
-
|
|
8
|
+
network;
|
|
9
|
+
tokens;
|
|
10
|
+
constructor(network = "mainnet", tokens) {
|
|
11
|
+
this.network = network;
|
|
12
|
+
this.tokens = tokens ?? DEFAULT_TOKENS;
|
|
13
|
+
}
|
|
14
|
+
async getChain() {
|
|
15
|
+
const chains = await import("viem/chains");
|
|
16
|
+
return this.network === "testnet" ? chains.tempoModerato : chains.tempo;
|
|
17
|
+
}
|
|
18
|
+
getChainId() {
|
|
19
|
+
// Tempo Mainnet: 4217, Testnet (Moderato): 42431
|
|
20
|
+
return this.network === "testnet" ? "eip155:42431" : "eip155:4217";
|
|
21
|
+
}
|
|
6
22
|
async openSession(params) {
|
|
7
23
|
const { service, maxBudget, walletId } = params;
|
|
24
|
+
const chainId = this.getChainId();
|
|
8
25
|
const wallet = getWallet(walletId);
|
|
9
26
|
const evmAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
|
|
10
27
|
if (!evmAccount) {
|
|
11
28
|
throw new Error(`Wallet '${walletId}' has no EVM account for Tempo`);
|
|
12
29
|
}
|
|
13
|
-
// create OWS-backed viem custom account for Tempo
|
|
14
30
|
const { toAccount } = await import("viem/accounts");
|
|
15
|
-
const { createPublicClient, http } = await import("viem");
|
|
16
|
-
const { tempo: tempoChain } = await import("viem/chains");
|
|
17
31
|
const owsAccount = toAccount({
|
|
18
32
|
address: evmAccount.address,
|
|
19
33
|
async signMessage({ message }) {
|
|
20
34
|
const msgStr = typeof message === "string"
|
|
21
35
|
? message
|
|
22
36
|
: Buffer.from(message.raw).toString("hex");
|
|
23
|
-
const result = signMessage(walletId,
|
|
37
|
+
const result = signMessage(walletId, chainId, msgStr);
|
|
24
38
|
return result.signature;
|
|
25
39
|
},
|
|
26
40
|
async signTransaction(tx) {
|
|
27
41
|
const { signTransaction: owsSignTx } = await import("@open-wallet-standard/core");
|
|
28
42
|
const txHex = typeof tx === "string" ? tx : JSON.stringify(tx);
|
|
29
|
-
const result = owsSignTx(walletId,
|
|
43
|
+
const result = owsSignTx(walletId, chainId, txHex);
|
|
30
44
|
return result.signature;
|
|
31
45
|
},
|
|
32
46
|
async signTypedData(typedData) {
|
|
33
47
|
const { signTypedData: owsSignTyped } = await import("@open-wallet-standard/core");
|
|
34
|
-
const result = owsSignTyped(walletId,
|
|
48
|
+
const result = owsSignTyped(walletId, chainId, JSON.stringify(typedData));
|
|
35
49
|
return result.signature;
|
|
36
50
|
},
|
|
37
51
|
});
|
|
38
|
-
// create the session manager with Tempo's built-in method
|
|
39
52
|
const manager = tempoClient.session({
|
|
40
53
|
account: owsAccount,
|
|
41
54
|
maxDeposit: String(maxBudget),
|
|
42
55
|
});
|
|
43
|
-
// open the session (creates on-chain channel)
|
|
44
56
|
await manager.open({ deposit: BigInt(Math.floor(maxBudget * 1e6)) });
|
|
45
57
|
const sessionId = manager.channelId ?? crypto.randomUUID();
|
|
46
58
|
this.sessions.set(sessionId, {
|
|
@@ -58,12 +70,13 @@ export class TempoConnector {
|
|
|
58
70
|
status: "active",
|
|
59
71
|
};
|
|
60
72
|
}
|
|
73
|
+
sessions = new Map();
|
|
61
74
|
async closeSession(sessionId) {
|
|
62
75
|
const session = this.sessions.get(sessionId);
|
|
63
76
|
if (!session) {
|
|
64
77
|
throw new Error(`Session '${sessionId}' not found`);
|
|
65
78
|
}
|
|
66
|
-
|
|
79
|
+
await session.manager.close();
|
|
67
80
|
this.sessions.delete(sessionId);
|
|
68
81
|
const totalSpent = Number(session.manager.cumulative) / 1e6;
|
|
69
82
|
return {
|
|
@@ -72,31 +85,52 @@ export class TempoConnector {
|
|
|
72
85
|
refund: session.maxBudget - totalSpent,
|
|
73
86
|
};
|
|
74
87
|
}
|
|
75
|
-
async
|
|
88
|
+
async getBalances(walletId) {
|
|
76
89
|
const wallet = getWallet(walletId);
|
|
77
90
|
const evmAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
|
|
78
91
|
if (!evmAccount) {
|
|
79
|
-
return
|
|
92
|
+
return this.tokens.map((t) => ({
|
|
93
|
+
chain: this.chain,
|
|
94
|
+
amount: 0,
|
|
95
|
+
currency: t.symbol,
|
|
96
|
+
}));
|
|
80
97
|
}
|
|
81
98
|
try {
|
|
82
|
-
const { createPublicClient, http, parseAbi } = await import("viem");
|
|
83
|
-
const
|
|
99
|
+
const { createPublicClient, http, parseAbi, formatUnits } = await import("viem");
|
|
100
|
+
const chain = await this.getChain();
|
|
84
101
|
const client = createPublicClient({
|
|
85
|
-
chain
|
|
102
|
+
chain,
|
|
86
103
|
transport: http(),
|
|
87
104
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
const balanceAbi = parseAbi([
|
|
106
|
+
"function balanceOf(address) view returns (uint256)",
|
|
107
|
+
]);
|
|
108
|
+
const results = await Promise.all(this.tokens.map(async (token) => {
|
|
109
|
+
try {
|
|
110
|
+
const balance = await client.readContract({
|
|
111
|
+
address: token.address,
|
|
112
|
+
abi: balanceAbi,
|
|
113
|
+
functionName: "balanceOf",
|
|
114
|
+
args: [evmAccount.address],
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
chain: this.chain,
|
|
118
|
+
amount: Number(formatUnits(balance, token.decimals ?? 6)),
|
|
119
|
+
currency: token.symbol,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { chain: this.chain, amount: 0, currency: token.symbol };
|
|
124
|
+
}
|
|
125
|
+
}));
|
|
126
|
+
return results;
|
|
97
127
|
}
|
|
98
128
|
catch {
|
|
99
|
-
return
|
|
129
|
+
return this.tokens.map((t) => ({
|
|
130
|
+
chain: this.chain,
|
|
131
|
+
amount: 0,
|
|
132
|
+
currency: t.symbol,
|
|
133
|
+
}));
|
|
100
134
|
}
|
|
101
135
|
}
|
|
102
136
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import type { Network, TokenConfig } from "../config.js";
|
|
1
2
|
export interface Connector {
|
|
2
3
|
chain: string;
|
|
4
|
+
network: Network;
|
|
5
|
+
tokens: TokenConfig[];
|
|
3
6
|
openSession(params: {
|
|
4
7
|
service: string;
|
|
5
8
|
maxBudget: number;
|
|
6
9
|
walletId: string;
|
|
7
10
|
}): Promise<Session>;
|
|
8
11
|
closeSession(sessionId: string): Promise<SessionResult>;
|
|
9
|
-
|
|
12
|
+
getBalances(walletId: string): Promise<Balance[]>;
|
|
10
13
|
}
|
|
11
14
|
export interface Session {
|
|
12
15
|
sessionId: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Operator } from "./operator.js";
|
|
|
2
2
|
export { Operator } from "./operator.js";
|
|
3
3
|
export type { PayParams } from "./operator.js";
|
|
4
4
|
export { loadConfig } from "./config.js";
|
|
5
|
-
export type { OperatorConfig, WalletConfig, NotificationConfig } from "./config.js";
|
|
5
|
+
export type { OperatorConfig, WalletConfig, NotificationConfig, TokenConfig, Network } from "./config.js";
|
|
6
6
|
export type { Connector, Session, SessionResult, Balance } from "./connectors/types.js";
|
|
7
7
|
export { TempoConnector } from "./connectors/tempo.js";
|
|
8
8
|
export { BaseConnector } from "./connectors/base.js";
|
package/dist/operator.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare class Operator {
|
|
|
17
17
|
start(): Promise<void>;
|
|
18
18
|
shutdown(): Promise<void>;
|
|
19
19
|
pay(walletName: string, params: PayParams): Promise<SessionResult>;
|
|
20
|
-
|
|
20
|
+
getBalances(walletName: string, chain: string): Promise<Balance[]>;
|
|
21
21
|
closeSession(walletName: string, sessionId: string): Promise<SessionResult>;
|
|
22
22
|
getWalletNames(): string[];
|
|
23
23
|
private getWalletConfig;
|
package/dist/operator.js
CHANGED
|
@@ -18,24 +18,30 @@ export class Operator {
|
|
|
18
18
|
this.initNotifications();
|
|
19
19
|
}
|
|
20
20
|
initConnectors() {
|
|
21
|
-
|
|
22
|
-
tempo: new TempoConnector(),
|
|
23
|
-
base: new BaseConnector(),
|
|
24
|
-
solana: new SolanaConnector(),
|
|
25
|
-
};
|
|
26
|
-
// register only the connectors that wallets actually use
|
|
27
|
-
const neededChains = new Set();
|
|
21
|
+
// connectors keyed by "chain:network" to support different networks per wallet
|
|
28
22
|
for (const wallet of Object.values(this.config.wallets)) {
|
|
23
|
+
const network = wallet.network ?? "mainnet";
|
|
29
24
|
for (const chain of wallet.chains) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
const key = `${chain}:${network}`;
|
|
26
|
+
if (this.connectors.has(key))
|
|
27
|
+
continue;
|
|
28
|
+
const tokens = wallet.tokens?.[chain];
|
|
29
|
+
let connector;
|
|
30
|
+
switch (chain) {
|
|
31
|
+
case "tempo":
|
|
32
|
+
connector = new TempoConnector(network, tokens);
|
|
33
|
+
break;
|
|
34
|
+
case "base":
|
|
35
|
+
connector = new BaseConnector(network, tokens);
|
|
36
|
+
break;
|
|
37
|
+
case "solana":
|
|
38
|
+
connector = new SolanaConnector(network, tokens);
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
throw new Error(`No built-in connector for chain '${chain}'`);
|
|
42
|
+
}
|
|
43
|
+
this.connectors.set(key, connector);
|
|
37
44
|
}
|
|
38
|
-
this.connectors.set(chain, connector);
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
initNotifications() {
|
|
@@ -126,17 +132,18 @@ export class Operator {
|
|
|
126
132
|
recordSpend(walletName, params.service, params.chain, result.totalSpent);
|
|
127
133
|
return result;
|
|
128
134
|
}
|
|
129
|
-
async
|
|
135
|
+
async getBalances(walletName, chain) {
|
|
130
136
|
const wallet = this.getWalletConfig(walletName);
|
|
131
137
|
const connector = this.getConnector(walletName, chain);
|
|
132
|
-
return connector.
|
|
138
|
+
return connector.getBalances(wallet.walletId);
|
|
133
139
|
}
|
|
134
140
|
async closeSession(walletName, sessionId) {
|
|
135
141
|
// find the connector that has this session
|
|
136
142
|
// try all connectors for this wallet
|
|
137
143
|
const wallet = this.getWalletConfig(walletName);
|
|
144
|
+
const network = wallet.network ?? "mainnet";
|
|
138
145
|
for (const chain of wallet.chains) {
|
|
139
|
-
const connector = this.connectors.get(chain);
|
|
146
|
+
const connector = this.connectors.get(`${chain}:${network}`);
|
|
140
147
|
if (!connector)
|
|
141
148
|
continue;
|
|
142
149
|
try {
|
|
@@ -165,9 +172,11 @@ export class Operator {
|
|
|
165
172
|
if (!wallet.chains.includes(chain)) {
|
|
166
173
|
throw new Error(`Wallet '${walletName}' is not configured for chain '${chain}'. Available: ${wallet.chains.join(", ")}`);
|
|
167
174
|
}
|
|
168
|
-
const
|
|
175
|
+
const network = wallet.network ?? "mainnet";
|
|
176
|
+
const key = `${chain}:${network}`;
|
|
177
|
+
const connector = this.connectors.get(key);
|
|
169
178
|
if (!connector) {
|
|
170
|
-
throw new Error(`No connector found for chain '${chain}'`);
|
|
179
|
+
throw new Error(`No connector found for chain '${chain}' on ${network}`);
|
|
171
180
|
}
|
|
172
181
|
return connector;
|
|
173
182
|
}
|
|
@@ -182,11 +191,14 @@ export class Operator {
|
|
|
182
191
|
const balances = {};
|
|
183
192
|
for (const chain of wallet.chains) {
|
|
184
193
|
try {
|
|
185
|
-
const
|
|
186
|
-
balances[chain] =
|
|
194
|
+
const chainBalances = await this.getBalances(walletName, chain);
|
|
195
|
+
balances[chain] = {};
|
|
196
|
+
for (const b of chainBalances) {
|
|
197
|
+
balances[chain][b.currency] = b.amount;
|
|
198
|
+
}
|
|
187
199
|
}
|
|
188
200
|
catch {
|
|
189
|
-
balances[chain] =
|
|
201
|
+
balances[chain] = {};
|
|
190
202
|
}
|
|
191
203
|
}
|
|
192
204
|
await this.notifications.notify(walletName, {
|
|
@@ -2,19 +2,21 @@ export async function checkLowBalance(walletName, walletId, chain, lowBalanceThr
|
|
|
2
2
|
if (lowBalanceThreshold === undefined)
|
|
3
3
|
return;
|
|
4
4
|
try {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
const balances = await connector.getBalances(walletId);
|
|
6
|
+
for (const balance of balances) {
|
|
7
|
+
if (balance.amount < lowBalanceThreshold) {
|
|
8
|
+
await provider.send({
|
|
9
|
+
type: "low_balance",
|
|
10
|
+
walletName,
|
|
11
|
+
chain,
|
|
12
|
+
data: {
|
|
13
|
+
balance: balance.amount,
|
|
14
|
+
currency: balance.currency,
|
|
15
|
+
threshold: lowBalanceThreshold,
|
|
16
|
+
},
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
}).catch(() => { });
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
catch {
|