razvan commited on
Commit
a7d22b3
Β·
verified Β·
1 Parent(s): 69bbae6

Upload builderbrain/circle_gateway_client.py

Browse files
Files changed (1) hide show
  1. builderbrain/circle_gateway_client.py +228 -0
builderbrain/circle_gateway_client.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Real Circle Gateway API Client
3
+ ==============================
4
+
5
+ Implements the actual Circle Gateway HTTP API for:
6
+ - Cross-chain USDC transfers (CCTP via Gateway)
7
+ - Unified balance queries
8
+ - Domain enumeration
9
+
10
+ API Endpoints (from docs):
11
+ - GET https://gateway-api-testnet.circle.com/v1/gateway-info
12
+ - POST https://gateway-api-testnet.circle.com/v1/balances
13
+ - POST https://gateway-api-testnet.circle.com/v1/transfer
14
+
15
+ Auth: Bearer <CIRCLE_API_KEY>
16
+
17
+ References:
18
+ - Circle Gateway Docs: https://developers.circle.com/gateway
19
+ - Nanopayments Blog: https://community.arc.network/public/clubs/agentic-economy/blogs/...
20
+ """
21
+
22
+ import requests
23
+ from dataclasses import dataclass
24
+ from typing import Dict, List, Optional
25
+ from datetime import datetime
26
+
27
+
28
+ @dataclass
29
+ class GatewayDomain:
30
+ """A supported domain (chain) from Gateway info."""
31
+ chain: str # e.g. "Ethereum"
32
+ network: str # e.g. "Sepolia"
33
+ domain: int # uint32 domain ID
34
+ wallet_contract: str
35
+ minter_contract: str
36
+ supported_tokens: List[str]
37
+
38
+
39
+ @dataclass
40
+ class GatewayBalance:
41
+ """USDC balance in a Gateway wallet contract."""
42
+ domain: int
43
+ depositor: str
44
+ balance: str # decimal string, e.g. "123.456789"
45
+
46
+
47
+ @dataclass
48
+ class TransferAttestation:
49
+ """Response from POST /v1/transfer (create transfer attestation)."""
50
+ transfer_id: str
51
+ attestation: str # hex-encoded bytes
52
+ signature: str # operator ECDSA signature
53
+ total_fees: str # decimal string in USDC
54
+ fee_token: str # "USDC"
55
+ per_intent_fees: List[Dict]
56
+ expiration_block: int
57
+
58
+
59
+ class CircleGatewayClient:
60
+ """
61
+ Production client for Circle Gateway API (testnet).
62
+
63
+ Usage:
64
+ client = CircleGatewayClient(api_key="sk_test_...")
65
+ domains = client.get_gateway_info()
66
+ balances = client.get_balances(depositor="0x...", domain=0)
67
+ attestation = client.create_transfer(...)
68
+ """
69
+
70
+ TESTNET_BASE = "https://gateway-api-testnet.circle.com"
71
+
72
+ def __init__(self, api_key: str, base_url: Optional[str] = None):
73
+ self.api_key = api_key
74
+ self.base_url = base_url or self.TESTNET_BASE
75
+ self.session = requests.Session()
76
+ self.session.headers.update({
77
+ "Authorization": f"Bearer {api_key}",
78
+ "Content-Type": "application/json",
79
+ })
80
+
81
+ # ────────────────────────────── Gateway Info ──────────────────────────────
82
+
83
+ def get_gateway_info(self) -> List[GatewayDomain]:
84
+ """
85
+ GET /v1/gateway-info
86
+
87
+ Returns supported domains with contract addresses.
88
+ Use this instead of hardcoding chains.
89
+ """
90
+ resp = self.session.get(
91
+ f"{self.base_url}/v1/gateway-info",
92
+ timeout=15,
93
+ )
94
+ resp.raise_for_status()
95
+ data = resp.json()
96
+
97
+ domains = []
98
+ for d in data.get("domains", []):
99
+ wc = d.get("walletContract", {})
100
+ mc = d.get("minterContract", {})
101
+ domains.append(GatewayDomain(
102
+ chain=d.get("chain", ""),
103
+ network=d.get("network", ""),
104
+ domain=d.get("domain", 0),
105
+ wallet_contract=wc.get("address", ""),
106
+ minter_contract=mc.get("address", ""),
107
+ supported_tokens=wc.get("supportedTokens", []),
108
+ ))
109
+ return domains
110
+
111
+ # ────────────────────────────── Balances ──────────────────────────────
112
+
113
+ def get_balances(
114
+ self,
115
+ depositor: str,
116
+ domain: int,
117
+ token: str = "USDC",
118
+ ) -> List[GatewayBalance]:
119
+ """
120
+ POST /v1/balances
121
+
122
+ Query unified USDC balance for a depositor on a domain.
123
+
124
+ Request:
125
+ {
126
+ "token": "USDC",
127
+ "sources": [
128
+ {"depositor": "0x...", "domain": 0}
129
+ ]
130
+ }
131
+ """
132
+ payload = {
133
+ "token": token,
134
+ "sources": [
135
+ {"depositor": depositor, "domain": domain}
136
+ ],
137
+ }
138
+
139
+ resp = self.session.post(
140
+ f"{self.base_url}/v1/balances",
141
+ json=payload,
142
+ timeout=15,
143
+ )
144
+ resp.raise_for_status()
145
+ data = resp.json()
146
+
147
+ balances = []
148
+ for b in data.get("balances", []):
149
+ balances.append(GatewayBalance(
150
+ domain=b.get("domain", 0),
151
+ depositor=b.get("depositor", ""),
152
+ balance=b.get("balance", "0"),
153
+ ))
154
+ return balances
155
+
156
+ # ────────────────────────────── Transfer Attestation ──────────────────────────────
157
+
158
+ def create_transfer_attestation(
159
+ self,
160
+ burn_intents: List[Dict],
161
+ ) -> TransferAttestation:
162
+ """
163
+ POST /v1/transfer
164
+
165
+ Create a transfer attestation for one or more burn intents.
166
+
167
+ WARNING: This is the LOW-LEVEL protocol API. Each burn intent requires:
168
+ - Source wallet domain + wallet contract address
169
+ - Destination minter domain + minter contract address
170
+ - Token addresses (source and dest)
171
+ - Debitor (depositor) and recipient addresses (32-byte padded)
172
+ - Amount, max fee, max block height, user salt
173
+ - ECDSA signature over encoded burn intent(s)
174
+
175
+ For high-level transfers, use Circle's App Kit or Bridge SDK instead.
176
+
177
+ This method is a PLACEHOLDER for the full encoded intent construction.
178
+ In production, you would:
179
+ 1. Fetch domain info from get_gateway_info()
180
+ 2. Encode burn intent per CCTP spec
181
+ 3. Sign with depositor's key (or Circle Wallet API)
182
+ 4. Submit encoded intent + signature
183
+ """
184
+ # TODO: Implement full burn intent encoding + signing
185
+ # For hackathon, this is intentionally left as a documented stub
186
+ # because encoding requires domain-specific contract addresses
187
+ # and depositor signatures that depend on your wallet setup.
188
+
189
+ resp = self.session.post(
190
+ f"{self.base_url}/v1/transfer",
191
+ json={"burnIntents": burn_intents},
192
+ timeout=30,
193
+ )
194
+ resp.raise_for_status()
195
+ data = resp.json()
196
+
197
+ return TransferAttestation(
198
+ transfer_id=data.get("transferId", ""),
199
+ attestation=data.get("attestation", ""),
200
+ signature=data.get("signature", ""),
201
+ total_fees=data.get("fees", {}).get("total", "0"),
202
+ fee_token=data.get("fees", {}).get("token", "USDC"),
203
+ per_intent_fees=data.get("fees", {}).get("perIntent", []),
204
+ expiration_block=data.get("expirationBlock", 0),
205
+ )
206
+
207
+ def mint_on_destination(
208
+ self,
209
+ minter_contract_address: str,
210
+ attestation: str,
211
+ signature: str,
212
+ ) -> Dict:
213
+ """
214
+ On-chain call: gatewayMinter.gatewayMint(attestation, signature)
215
+
216
+ This is an on-chain contract call, NOT a REST API call.
217
+ Use your Web3/Ethers client to call the minter contract on the
218
+ destination chain.
219
+
220
+ Returns transaction hash.
221
+ """
222
+ # This is a documentation-only method; actual implementation
223
+ # requires Web3 provider + contract ABI + depositor wallet
224
+ raise NotImplementedError(
225
+ "On-chain minting requires Web3 provider. "
226
+ "Call gatewayMinter.gatewayMint(attestation, signature) "
227
+ "on the destination chain's minter contract."
228
+ )