@nexus-cross/pop 1.3.10-beta.2 → 1.4.0-beta.2
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/AGENTS.md +35 -44
- package/README.md +54 -49
- package/dist/adapters/index.d.ts +375 -588
- package/dist/adapters/index.js +1 -1
- package/dist/chunk-SE2HF5WH.js +1 -0
- package/dist/{createSafeDrop-qoNG59jB.d.ts → createSafeDrop-CayUX4w_.d.ts} +266 -118
- package/dist/index.d.ts +10 -44
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/infrastructure/openapi.json +836 -116
- package/dist/chunk-KEW4FB4S.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -80,7 +80,6 @@ const safeDrop = createSafeDropClient({
|
|
|
80
80
|
walletClient,
|
|
81
81
|
chain,
|
|
82
82
|
claimBaseUrl: 'https://app.example/claim',
|
|
83
|
-
permit: false, // true only if BOTH the deployment and the token support EIP-2612
|
|
84
83
|
api: { getJwt: () => token },
|
|
85
84
|
});
|
|
86
85
|
```
|
|
@@ -94,7 +93,7 @@ The provider only carries the client through context — no connection logic liv
|
|
|
94
93
|
|
|
95
94
|
```ts
|
|
96
95
|
const envelopes = await safeDrop.listEnvelopes(); // optional card designs
|
|
97
|
-
const
|
|
96
|
+
const deposit = await safeDrop.deposit({
|
|
98
97
|
sender: address,
|
|
99
98
|
recipient: { provider: 'x', handle: 'theo_13303' },
|
|
100
99
|
token: ERC20_ADDRESS,
|
|
@@ -102,23 +101,25 @@ const { withdrawLink, claimAddress } = await safeDrop.deposit({
|
|
|
102
101
|
message: 'Happy birthday!', // optional, <= 140 runes
|
|
103
102
|
envelopeId: envelopes[0]?.id, // optional
|
|
104
103
|
});
|
|
104
|
+
if (!deposit.isMapped) {
|
|
105
|
+
location.assign(buildComposeUrl(buildDmText(deposit.withdrawLink), recipientId));
|
|
106
|
+
}
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
`location.assign(buildComposeUrl(buildDmText(withdrawLink), recipientId))`.
|
|
109
|
+
Mapped deposits have no claim link because the mapped recipient receives them directly.
|
|
109
110
|
|
|
110
111
|
### Claim (recipient, on the claim route)
|
|
111
112
|
|
|
112
113
|
```ts
|
|
113
|
-
const { sender, secret } = parseWithdrawLink(location.href);
|
|
114
|
+
const { sender, claimAddress, secret } = parseWithdrawLink(location.href);
|
|
114
115
|
const { oauth } = await xauth.complete(location.href); // X OAuth2 PKCE callback
|
|
115
|
-
const
|
|
116
|
-
|
|
116
|
+
const claim = await safeDrop.retrieveClaimKey({ claimAddress: claimAddress!, senderAddress: sender!, oauth });
|
|
117
|
+
if (claim.isMapped) throw new Error('Use the mapped withdrawal flow');
|
|
118
|
+
await safeDrop.withdraw({ recipient: myAddress, sponsor: sender!, claimAddress, secret: secret!, claimKey: claim.claimKey });
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
`retrieveClaimKey` is the point of no return — after it resolves, key custody is the client's.
|
|
120
|
-
|
|
121
|
-
`claimAddress` selector; multiple drops from the same sender are claimed one link at a time.
|
|
122
|
+
`claimAddress` selects the exact pending drop; OAuth proves entitlement to that drop's handle.
|
|
122
123
|
|
|
123
124
|
### Inbox / social surfaces
|
|
124
125
|
|
|
@@ -126,8 +127,8 @@ It returns **only the latest pending drop** for that `(sender, handle)` pair and
|
|
|
126
127
|
await safeDrop.connectX({ oauth }); // required once before listDrops
|
|
127
128
|
const conn = await safeDrop.getXConnection(); // null when not connected
|
|
128
129
|
const inbox = await safeDrop.listDrops(); // { items, count, totalAmount, hasClaimedBefore }
|
|
129
|
-
const top = await safeDrop.getLeaderboard({ token: ERC20_ADDRESS });
|
|
130
|
-
const page = await safeDrop.listHistories({
|
|
130
|
+
const top = await safeDrop.getLeaderboard({ token: ERC20_ADDRESS, page: 1, pageSize: 10 });
|
|
131
|
+
const page = await safeDrop.listHistories({ type: 'received', page: 1, pageSize: 20 });
|
|
131
132
|
await safeDrop.disconnectX();
|
|
132
133
|
```
|
|
133
134
|
|
|
@@ -137,7 +138,7 @@ batch-claim CTA on it.
|
|
|
137
138
|
### Refund
|
|
138
139
|
|
|
139
140
|
```ts
|
|
140
|
-
await safeDrop.refund({ claimAddress });
|
|
141
|
+
await safeDrop.refund({ claimAddress, sponsor: address });
|
|
141
142
|
```
|
|
142
143
|
|
|
143
144
|
### Batch claim — drain every drop for a handle in one flow
|
|
@@ -146,33 +147,25 @@ await safeDrop.refund({ claimAddress }); // sponsor only, after expiry
|
|
|
146
147
|
const { txHashes, recipient } = await safeDrop.batchClaim({
|
|
147
148
|
id: myHandle, // normalized internally
|
|
148
149
|
oauth, // X OAuth proof (handle ownership)
|
|
149
|
-
|
|
150
|
+
sponsor: sender,
|
|
151
|
+
claimAddress,
|
|
152
|
+
claimKey,
|
|
153
|
+
secret,
|
|
150
154
|
});
|
|
151
155
|
```
|
|
152
156
|
|
|
153
157
|
`batchClaim` does the whole sequence in the order the contract enforces:
|
|
154
|
-
read `validatorNonceById(keccak256(utf8(id)))` →
|
|
155
|
-
|
|
156
|
-
recovers to `validator()` before spending gas** → call `withdrawByValidator` repeatedly until
|
|
157
|
-
`liveDropCountById(id)` is 0, reusing the one signature.
|
|
158
|
+
resolve the anchor and pending drop keys → read `validatorNonceById(keccak256(utf8(id)))` →
|
|
159
|
+
request and verify the backend signature → call `withdrawUnmappedBatchByKeys` once.
|
|
158
160
|
|
|
159
161
|
Rules specific to this path:
|
|
160
162
|
|
|
161
163
|
- **The recipient is the SIWE-authenticated address**, not a parameter you choose. Passing a
|
|
162
164
|
different `recipient` fails with `SIGN_FAILED`. To claim to another wallet, do SIWE with it.
|
|
163
|
-
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
- `maxCount` is a page size and is **not** covered by the signature — that is why one signature
|
|
168
|
-
survives every page. Do not re-request a signature per page.
|
|
169
|
-
- `hasClaimedBefore === false` (from `listDrops`) means the first claim must be an individual
|
|
170
|
-
claim; do not offer batch claim yet. The SDK also enforces this on-chain: batch claim aborts
|
|
171
|
-
with `DROP_NOT_FOUND` when `claimedRecipientOf(id)` is zero.
|
|
172
|
-
- The SDK cross-checks the signed recipient against `claimedRecipientOf(id)` and refuses to send
|
|
173
|
-
when they differ (`SIGN_FAILED`). This is a client-side guard — whether the contract enforces
|
|
174
|
-
it is **not verified** (the source is unverified and the signature check runs first), so do not
|
|
175
|
-
weaken or bypass it.
|
|
165
|
+
- The anchor temporary wallet submits the transaction with the configured sponsored fee path.
|
|
166
|
+
- The anchor drop's `secret` and `claimKey` are required in addition to the validator signature.
|
|
167
|
+
- A successful batch consumes the nonce and signature. Request a fresh signature for another call.
|
|
168
|
+
- Each page gets a fresh nonce and validator signature; `maxCount` defaults to 30.
|
|
176
169
|
- On `SIGN_FAILED` the backend signed something the contract will reject — a different EIP-712
|
|
177
170
|
domain/type, a stale nonce, or a `personal_sign` prefix. Do not retry; report it.
|
|
178
171
|
|
|
@@ -182,8 +175,8 @@ The on-chain contract (verified against deployed bytecode, CROSS testnet 612044)
|
|
|
182
175
|
domain { name: 'ONEpop', version: '1', chainId, verifyingContract: <SafeDrop> }
|
|
183
176
|
type ValidatorClaim(address recipient,string id,uint256 nonce,uint256 deadline)
|
|
184
177
|
nonce validatorNonceById(bytes32 keccak256(utf8(id))) // hashed key, NOT the raw string
|
|
185
|
-
submit
|
|
186
|
-
|
|
178
|
+
submit withdrawUnmappedBatchByKeys(recipient, id, anchorDropKey, claimSignature,
|
|
179
|
+
secret, nonce, deadline, dropKeys, validatorSignature)
|
|
187
180
|
errors ValidatorSigExpired() → deadline passed | InvalidValidatorNonce() → stale nonce
|
|
188
181
|
ECDSAInvalidSignature() → signer is not validator()
|
|
189
182
|
```
|
|
@@ -207,33 +200,31 @@ The SDK does not wrap it — use `EventSource` directly. Delivery is best-effort
|
|
|
207
200
|
logs, no analytics, no backend call. Sending it collapses the trust split.
|
|
208
201
|
3. **`getJwt` is mandatory** for `deposit`, `listDrops`, `listHistories`, and all `x-connections`
|
|
209
202
|
calls. Without it those reject with `UNAUTHORIZED` before any request is sent.
|
|
210
|
-
4.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
4. **The current deployment requires EIP-2612 permit.** The token must support EIP-2612.
|
|
204
|
+
5. **Keep `transactionFees` as EIP-1559 with tip ≥ 1 gwei.** Every write supplies an explicit
|
|
205
|
+
gas limit. Batch gas is `batchBaseGas + batchGasPerDrop * count` and may not exceed
|
|
206
|
+
`maxBatchGas`; calibrate these values below the chain block gas limit.
|
|
214
207
|
6. **`message` ≤ 140 runes** (code points) and `envelopeId` must come from `listEnvelopes()`;
|
|
215
208
|
otherwise the backend rejects with `INVALID_PARAM` / `ENVELOPE_NOT_FOUND`.
|
|
216
209
|
7. **Handles are normalized** by the SDK (`normalizeHandle`); pass the same handle to deposit and
|
|
217
210
|
to the API — do not hand-roll a second normalization.
|
|
218
|
-
8.
|
|
219
|
-
returns (the permit deployment returns five fields, no expiry). `expiry === undefined` means
|
|
220
|
-
*unknown*, not *not expired* — never drive refund/expiry UI off a missing value.
|
|
211
|
+
8. **Drops are keyed by bytes32.** Resolve `activeDropOf(claimAddress)` before `drops(dropKey)`.
|
|
221
212
|
|
|
222
213
|
## 7. API surface
|
|
223
214
|
|
|
224
215
|
| `safeDrop` method | Endpoint / chain call | Auth |
|
|
225
216
|
|---|---|---|
|
|
226
|
-
| `deposit` | `POST /wallets` + `
|
|
217
|
+
| `deposit` | `POST /wallets` + permit `deposit` | JWT |
|
|
227
218
|
| `retrieveClaimKey` | `POST /wallets/private-key` | X OAuth token (body) |
|
|
228
219
|
| `withdraw` | `withdraw(...)` from the claim wallet | claim key |
|
|
229
|
-
| `refund` | `
|
|
230
|
-
| `getDrop` | `
|
|
220
|
+
| `refund` | `activeDropOf(claimAddr)` + `refund(dropKey)` | sponsor wallet |
|
|
221
|
+
| `getDrop` | `activeDropOf(claimAddr)` + `drops(dropKey)` | — |
|
|
231
222
|
| `listDrops` | `GET /drops` | JWT + active X connection |
|
|
232
223
|
| `listEnvelopes` | `GET /envelopes` | public |
|
|
233
224
|
| `getLeaderboard` | `GET /leaderboards?token=` | public |
|
|
234
225
|
| `listHistories` | `GET /histories` | JWT |
|
|
235
226
|
| `getXConnection` / `connectX` / `disconnectX` | `GET`/`POST`/`DELETE /x-connections` | JWT |
|
|
236
|
-
| `batchClaim` | `POST /batch-claim-signature` + `
|
|
227
|
+
| `batchClaim` | `POST /batch-claim-signature` + `withdrawUnmappedBatchByKeys` | JWT + X OAuth token + anchor claim key |
|
|
237
228
|
| `requestBatchClaimSignature` | `POST /batch-claim-signature` (low-level; prefer `batchClaim`) | JWT + X OAuth token |
|
|
238
229
|
|
|
239
230
|
## 8. Errors
|
|
@@ -265,4 +256,4 @@ Everything rejects with `SafeDropError` — branch on `err.code`, never on the m
|
|
|
265
256
|
`buildWithdrawLink` (the `#fragment` placement is load-bearing).
|
|
266
257
|
- Injecting a partial `SafeDropApiPort` mock and calling the query methods → `API_ERROR`
|
|
267
258
|
("not implemented"); implement the methods your test path touches.
|
|
268
|
-
-
|
|
259
|
+
- Using a token without EIP-2612 permit support.
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ npm i @nexus-cross/pop viem # react는 선택
|
|
|
24
24
|
|
|
25
25
|
## 개념
|
|
26
26
|
|
|
27
|
-
- **claimAddress** — 드롭당 임시 지갑 주소.
|
|
27
|
+
- **claimAddress** — 드롭당 임시 지갑 주소. `activeDropOf`로 현재 drop key를 찾는 데 사용.
|
|
28
28
|
- **secret** — 평문 비밀 문구. **어떤 서버에도 전송되지 않고** 수령 링크의 `#fragment`에만 담긴다.
|
|
29
29
|
- **withdrawLink** — `…/claim?sender=<addr>#<secret>`. 이 링크를 X DM으로 수령인에게 보낸다.
|
|
30
30
|
- **claimKey** — 임시 claim 지갑의 개인키. 수령인이 X OAuth 검증 후 백엔드에서 받아 가스 대납 withdraw에 쓴다.
|
|
@@ -65,7 +65,9 @@ const { token } = await auth.siweToken(address, signature); // token = getJwt
|
|
|
65
65
|
## 1) Deposit — 토큰 보내기
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
|
-
|
|
68
|
+
import { buildComposeUrl, buildDmText } from '@nexus-cross/pop';
|
|
69
|
+
|
|
70
|
+
const deposit = await safeDrop.deposit({
|
|
69
71
|
sender: address, // 연결된 지갑 = 스폰서
|
|
70
72
|
recipient: { provider: 'x', handle: 'theo_13303' },
|
|
71
73
|
token: '0x…ERC20',
|
|
@@ -74,18 +76,16 @@ const { withdrawLink, claimAddress } = await safeDrop.deposit({
|
|
|
74
76
|
envelopeId: 'classic-coral', // 선택 — listEnvelopes()의 id
|
|
75
77
|
// secret 미지정 시 자동 생성
|
|
76
78
|
});
|
|
79
|
+
if (!deposit.isMapped) {
|
|
80
|
+
location.assign(buildComposeUrl(buildDmText(deposit.withdrawLink), recipientId));
|
|
81
|
+
}
|
|
77
82
|
```
|
|
78
83
|
|
|
79
84
|
`message`·`envelopeId`는 **백엔드에만 저장**되는 메타데이터다(컨트랙트에 필드 없음).
|
|
80
85
|
|
|
81
|
-
내부 흐름: 임시 claim 지갑 생성(백엔드) →
|
|
82
|
-
|
|
83
|
-
수령인에게 링크 전달 (X DM 작성창):
|
|
86
|
+
내부 흐름: 임시 claim 지갑 생성(백엔드) → EIP-2612 permit 서명 → `deposit` → 링크 생성. `amount`는 **BigInt로 끝까지** 유지한다 (Number 변환 금지).
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
import { buildDmText, buildComposeUrl } from '@nexus-cross/pop';
|
|
87
|
-
location.assign(buildComposeUrl(buildDmText(withdrawLink), recipientId));
|
|
88
|
-
```
|
|
88
|
+
`isMapped: true`면 이미 매핑된 수령인으로 즉시 예치되어 claim link가 없다.
|
|
89
89
|
|
|
90
90
|
## 2) Withdraw — 수령인이 받기
|
|
91
91
|
|
|
@@ -95,7 +95,7 @@ location.assign(buildComposeUrl(buildDmText(withdrawLink), recipientId));
|
|
|
95
95
|
import { parseWithdrawLink } from '@nexus-cross/pop';
|
|
96
96
|
import { XAuthClient, HttpXAuthAdapter } from '@nexus-cross/pop/adapters';
|
|
97
97
|
|
|
98
|
-
const { sender, secret } = parseWithdrawLink(location.href);
|
|
98
|
+
const { sender, claimAddress, secret } = parseWithdrawLink(location.href);
|
|
99
99
|
|
|
100
100
|
// (a) X OAuth2 PKCE — 리다이렉트는 앱이 담당
|
|
101
101
|
const xauth = new XAuthClient({
|
|
@@ -108,37 +108,38 @@ const xauth = new XAuthClient({
|
|
|
108
108
|
const { oauth } = await xauth.complete(location.href);
|
|
109
109
|
|
|
110
110
|
// (b) OAuth 검증 → 임시 claim 키/주소 수령 (point of return)
|
|
111
|
-
const
|
|
111
|
+
const claim = await safeDrop.retrieveClaimKey({
|
|
112
|
+
claimAddress: claimAddress!,
|
|
112
113
|
senderAddress: sender!,
|
|
113
114
|
oauth,
|
|
114
115
|
});
|
|
116
|
+
if (claim.isMapped) throw new Error('Use the mapped withdrawal flow');
|
|
115
117
|
|
|
116
118
|
// (c) 가스 대납 withdraw — 임시 지갑이 직접 tx 전송
|
|
117
119
|
const { txHash } = await safeDrop.withdraw({
|
|
118
120
|
recipient: myReceivingAddress, // 수령인이 지정, 서명에 바인딩
|
|
121
|
+
sponsor: sender!,
|
|
119
122
|
claimAddress,
|
|
120
123
|
secret: secret!,
|
|
121
|
-
claimKey,
|
|
124
|
+
claimKey: claim.claimKey,
|
|
122
125
|
});
|
|
123
126
|
```
|
|
124
127
|
|
|
125
|
-
## 3) Refund —
|
|
128
|
+
## 3) Refund — sponsor 환불
|
|
126
129
|
|
|
127
130
|
```ts
|
|
128
|
-
await safeDrop.refund({ claimAddress });
|
|
131
|
+
await safeDrop.refund({ claimAddress, sponsor: address });
|
|
129
132
|
```
|
|
130
133
|
|
|
131
134
|
## 조회 — 드롭 상태 (온체인)
|
|
132
135
|
|
|
133
136
|
```ts
|
|
134
|
-
const drop = await safeDrop.getDrop(claimAddress); // 없으면 null
|
|
135
|
-
// drop: DropState { claimAddress, sponsor, amount, secretHash,
|
|
137
|
+
const drop = await safeDrop.getDrop(claimAddress, sponsor); // 없으면 null
|
|
138
|
+
// drop: DropState { dropKey, claimAddress, sponsor, recipient, amount, secretHash, id, token }
|
|
136
139
|
```
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
`token`은 에스크로 `token()`으로 채우지만, **`expiry`는 5필드 배포본에서 `undefined`** 다.
|
|
141
|
-
만료 판정 전에 존재 여부를 확인하라(`undefined` = 만료 아님이 아니라 *알 수 없음*).
|
|
141
|
+
현재 컨트랙트는 `activeDropOf(claimAddress)`로 key를 찾고 `drops(dropKey)`를 조회한다.
|
|
142
|
+
토큰은 드롭별 필드가 아니라 에스크로의 고정 `token()` 값이다.
|
|
142
143
|
|
|
143
144
|
## 4) 백엔드 조회 — 인박스 · X 연결 · 리더보드 · 히스토리
|
|
144
145
|
|
|
@@ -152,15 +153,15 @@ await safeDrop.disconnectX(); // idempotent
|
|
|
152
153
|
|
|
153
154
|
// 내게 온 수령 대기 목록 (Bearer JWT + 활성 X 연결)
|
|
154
155
|
const inbox = await safeDrop.listDrops({ token: '0x…ERC20' });
|
|
155
|
-
// { items
|
|
156
|
-
// PendingDrop { claimAddress, token, amount
|
|
156
|
+
// { items, count, totalAmount, hasClaimedBefore, mappedRecipient, pendingChangeTo }
|
|
157
|
+
// PendingDrop { id, dropKey, claimAddress, token, amount, message, envelopeId, depositedAt, sender }
|
|
157
158
|
|
|
158
159
|
// 카드 디자인 · 리더보드 (공개 — JWT 불필요)
|
|
159
160
|
const envelopes = await safeDrop.listEnvelopes({ locale: 'ko' });
|
|
160
|
-
const top = await safeDrop.getLeaderboard({ token: '0x…ERC20' });
|
|
161
|
+
const top = await safeDrop.getLeaderboard({ token: '0x…ERC20', page: 1, pageSize: 10 });
|
|
161
162
|
|
|
162
|
-
// 내
|
|
163
|
-
const page = await safeDrop.listHistories({
|
|
163
|
+
// 내 drop 히스토리
|
|
164
|
+
const page = await safeDrop.listHistories({ type: 'received', page: 1, pageSize: 20 });
|
|
164
165
|
```
|
|
165
166
|
|
|
166
167
|
`inbox.hasClaimedBefore === false`면 **첫 수령은 개별 claim만** 가능하다 — 일괄 수령 CTA를 이 값으로 게이트한다.
|
|
@@ -169,26 +170,25 @@ const page = await safeDrop.listHistories({ event: 'Withdrawn', page: 1, pageSiz
|
|
|
169
170
|
|
|
170
171
|
```ts
|
|
171
172
|
const { txHashes, recipient } = await safeDrop.batchClaim({
|
|
172
|
-
id: myHandle,
|
|
173
|
-
oauth,
|
|
174
|
-
|
|
173
|
+
id: myHandle,
|
|
174
|
+
oauth,
|
|
175
|
+
sponsor: sender,
|
|
176
|
+
claimAddress,
|
|
177
|
+
claimKey,
|
|
178
|
+
secret,
|
|
175
179
|
});
|
|
176
180
|
```
|
|
177
181
|
|
|
178
|
-
컨트랙트가 강제하는 순서대로 진행한다:
|
|
179
|
-
`validatorNonceById
|
|
180
|
-
`
|
|
181
|
-
복원되는지 가스 쓰기 전에 검증** → `liveDropCountById(id)`가 0이 될 때까지
|
|
182
|
-
`withdrawByValidator` 반복(서명 1건 재사용).
|
|
182
|
+
컨트랙트가 강제하는 순서대로 진행한다: anchor drop key와 pending keys 조회 →
|
|
183
|
+
`validatorNonceById` → 백엔드 서명 → 서명자 검증 →
|
|
184
|
+
`withdrawUnmappedBatchByKeys` 1회 호출. validator 서명은 성공한 호출마다 소비된다.
|
|
183
185
|
|
|
184
186
|
| 개별 수령과 다른 점 | 내용 |
|
|
185
187
|
|---|---|
|
|
186
|
-
| secret |
|
|
188
|
+
| secret | anchor drop의 secret/claim key가 필요하다 |
|
|
187
189
|
| 수령 주소 | **SIWE 인증 주소로 고정**. 다른 주소를 넘기면 `SIGN_FAILED`(그 주소로 SIWE 재로그인해야 함) |
|
|
188
|
-
| tx 전송자 |
|
|
189
|
-
| 서명 재사용 |
|
|
190
|
-
| 첫 수령 | 개별 claim이 선행돼야 한다 — 온체인 `claimedRecipientOf(id)`가 zero면 `DROP_NOT_FOUND`로 차단 |
|
|
191
|
-
| 목적지 교차검증 | 서명된 recipient ≠ `claimedRecipientOf(id)`면 서명이 유효해도 전송하지 않음(`SIGN_FAILED`) |
|
|
190
|
+
| tx 전송자 | anchor 임시 claim 지갑 |
|
|
191
|
+
| 서명 재사용 | 불가 — nonce는 성공한 batch 호출마다 소비됨 |
|
|
192
192
|
|
|
193
193
|
`SIGN_FAILED`가 나면 백엔드 서명이 컨트랙트가 기대하는 값과 다르다(EIP-712 domain/type 불일치,
|
|
194
194
|
낡은 nonce, `personal_sign` 프리픽스). 재시도해도 동일하게 실패하니 그대로 노출한다.
|
|
@@ -200,9 +200,8 @@ const { txHashes, recipient } = await safeDrop.batchClaim({
|
|
|
200
200
|
domain { name: 'ONEpop', version: '1', chainId, verifyingContract: <SafeDrop> }
|
|
201
201
|
type ValidatorClaim(address recipient,string id,uint256 nonce,uint256 deadline)
|
|
202
202
|
nonce validatorNonceById(bytes32) ← keccak256(utf8(id)). **원문 string 아님**
|
|
203
|
-
submit
|
|
204
|
-
|
|
205
|
-
고정 claimedRecipientOf(string id) → 개별 수령으로 확정된 주소(미수령이면 zero)
|
|
203
|
+
submit withdrawUnmappedBatchByKeys(recipient, id, anchorDropKey, claimSignature,
|
|
204
|
+
secret, nonce, deadline, dropKeys, validatorSignature)
|
|
206
205
|
검증 순서 deadline(ValidatorSigExpired) → nonce(InvalidValidatorNonce) → 서명(ECDSAInvalidSignature)
|
|
207
206
|
```
|
|
208
207
|
|
|
@@ -211,8 +210,7 @@ submit withdrawByValidator(address recipient, string id, uint256 nonce,
|
|
|
211
210
|
두 조건을 클라이언트에서 선제 차단하지만 온체인 강제의 대체물은 아니다 —
|
|
212
211
|
`docs/pop/02-frontend-dependencies.md#검증-한계-중요` 참조.
|
|
213
212
|
|
|
214
|
-
|
|
215
|
-
`adapters/validatorAbi.ts`에 바이트코드로 검증한 ABI를 따로 들고 있다.
|
|
213
|
+
전체 현재 ABI는 `safedrop_abi.json`에서 직접 export된다.
|
|
216
214
|
서명만 직접 받아 쓰려면 저수준 `safeDrop.requestBatchClaimSignature({ id, oauth, nonce, deadline })`.
|
|
217
215
|
</details>
|
|
218
216
|
|
|
@@ -281,11 +279,12 @@ override 미지정 시 environment로 기본 URL을 고른다. base URL은 https
|
|
|
281
279
|
|---|---|---|---|
|
|
282
280
|
| `contractAddress` | `Address` | — (필수) | SafeDrop 컨트랙트 주소 |
|
|
283
281
|
| `publicClient` | viem `PublicClient` | — (필수) | 읽기·getDrop·digest |
|
|
284
|
-
| `walletClient` | viem `WalletClient` | — (필수) |
|
|
282
|
+
| `walletClient` | viem `WalletClient` | — (필수) | deposit/refund 서명(연결 지갑) |
|
|
285
283
|
| `chain` | viem `Chain` | — (필수) | 대상 체인 |
|
|
286
284
|
| `claimBaseUrl` | `string` | — (필수) | 수령 링크 base URL |
|
|
287
285
|
| `transport` | viem `Transport` | `http()`(chain.rpcUrls) | withdraw 임시 지갑 client RPC |
|
|
288
|
-
| `
|
|
286
|
+
| `transactionFees` | `SafeDropTransactionFees` | 아래 gas-abstraction 기본 | 모든 write의 EIP-1559/gas override |
|
|
287
|
+
| `withdrawFees` | `SafeDropTransactionFees` | — | deprecated alias |
|
|
289
288
|
| `api` | `HttpSafeDropApiAdapterOptions` | — | one-pop-api 어댑터 옵션(아래) |
|
|
290
289
|
| `apiPort` | `SafeDropApiPort` | — | 직접 구현/mock 주입(지정 시 `api` 무시) |
|
|
291
290
|
|
|
@@ -298,17 +297,23 @@ override 미지정 시 environment로 기본 URL을 고른다. base URL은 https
|
|
|
298
297
|
| `fetchImpl` | `typeof fetch` | `globalThis.fetch` | 테스트/SSR 주입 |
|
|
299
298
|
| `paths` | `Partial<PopApiPaths>` | `DEFAULT_POP_API_PATHS` (`/wallets`, `/wallets/private-key`, `/drops`, `/envelopes`, `/leaderboards`, `/histories`, `/x-connections`, `/batch-claim-signature`, `/events/subscribe`) | 엔드포인트 경로 override |
|
|
300
299
|
|
|
301
|
-
### `ViemSafeDropChainAdapter.
|
|
300
|
+
### `ViemSafeDropChainAdapter.transactionFees` 기본값
|
|
302
301
|
|
|
303
|
-
|
|
302
|
+
모든 write는 `type: 'eip1559'`와 gas limit을 명시해 wallet/RPC gas estimation을 피한다.
|
|
303
|
+
batch gas는 `batchBaseGas + batchGasPerDrop × count`이며 `maxBatchGas` 초과 시 전송 전에 실패한다.
|
|
304
304
|
|
|
305
305
|
```
|
|
306
|
-
gas:
|
|
306
|
+
gas: 1_000_000n
|
|
307
|
+
batchBaseGas: 1_000_000n
|
|
308
|
+
batchGasPerDrop: 750_000n
|
|
309
|
+
maxBatchGas: 30_000_000n
|
|
307
310
|
maxFeePerGas: 4_000_000_000n // 4 gwei
|
|
308
311
|
maxPriorityFeePerGas: 1_000_000_000n // 1 gwei (노드 최소 tip)
|
|
309
312
|
```
|
|
310
313
|
|
|
311
|
-
|
|
314
|
+
기본 batch 30건은 `23_500_000` gas다(CROSS testnet block gas limit `105_000_000` 아래).
|
|
315
|
+
실제 배포 환경의 block gas limit과 실행량에 맞춰 네 값을 함께 조정해야 하며,
|
|
316
|
+
tip ≥ 1 gwei의 type-2를 유지해야 sponsor 과금이 동작한다.
|
|
312
317
|
|
|
313
318
|
### `CrossAuthClient(options)` — SIWE JWT 발급 (`getJwt`용)
|
|
314
319
|
|