@gvnrdao/dh-sdk 0.0.314 → 0.0.315

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/index.js CHANGED
@@ -9178,6 +9178,23 @@ function createPKPManager(config) {
9178
9178
  // src/modules/loan/loan-creator.module.ts
9179
9179
  var import_ethers11 = require("ethers");
9180
9180
  var import_dh_lit_actions = __toESM(require_pkg_src());
9181
+
9182
+ // src/utils/contract-wallet.utils.ts
9183
+ var contractWalletCache = /* @__PURE__ */ new Map();
9184
+ async function isContractWallet(provider, address) {
9185
+ const { chainId } = await provider.getNetwork();
9186
+ const cacheKey = `${chainId}:${address.toLowerCase()}`;
9187
+ const cached = contractWalletCache.get(cacheKey);
9188
+ if (cached !== void 0)
9189
+ return cached;
9190
+ const code = await provider.getCode(address);
9191
+ const isContract = code !== "0x";
9192
+ contractWalletCache.set(cacheKey, isContract);
9193
+ return isContract;
9194
+ }
9195
+
9196
+ // src/modules/loan/loan-creator.module.ts
9197
+ var CONTRACT_WALLET_POLL_INTERVAL_MS = 4e3;
9181
9198
  var LoanCreator = class {
9182
9199
  config;
9183
9200
  transactionTimeoutMs;
@@ -9401,6 +9418,11 @@ var LoanCreator = class {
9401
9418
  validatorSignature: validatorSignature.substring(0, 20) + "..."
9402
9419
  });
9403
9420
  }
9421
+ this.config.onPkpCreated?.({
9422
+ tokenId: pkpData.tokenId,
9423
+ ethAddress: pkpData.ethAddress,
9424
+ publicKey: pkpData.publicKey
9425
+ });
9404
9426
  const addressesResult = await this.deriveBitcoinAddresses(
9405
9427
  pkpData.publicKey
9406
9428
  );
@@ -9573,6 +9595,12 @@ var LoanCreator = class {
9573
9595
  error: encErr instanceof Error ? encErr.message : String(encErr)
9574
9596
  });
9575
9597
  }
9598
+ const provider = this.config.contractManager.getProvider();
9599
+ const senderIsContractWallet = await isContractWallet(
9600
+ provider,
9601
+ await signerResult.value.getAddress()
9602
+ );
9603
+ const searchFromBlock = senderIsContractWallet ? await provider.getBlockNumber() : 0;
9576
9604
  const tx = await positionManager.createPosition(
9577
9605
  pkpIdBytes32,
9578
9606
  validatorSignature,
@@ -9594,19 +9622,20 @@ var LoanCreator = class {
9594
9622
  }
9595
9623
  if (this.config.debug) {
9596
9624
  log.info("\u23F3 Waiting for transaction confirmation", {
9597
- hash: tx.hash
9625
+ // For a contract wallet this is the wallet's own id, not an L1 hash — which is
9626
+ // exactly why the confirmation below does not wait on it.
9627
+ hash: tx.hash,
9628
+ senderIsContractWallet,
9629
+ searchFromBlock: senderIsContractWallet ? searchFromBlock : void 0
9598
9630
  });
9599
9631
  }
9600
9632
  const confirmations = await this.createPositionConfirmations();
9601
- const receipt = await Promise.race([
9602
- tx.wait(confirmations),
9603
- new Promise(
9604
- (_, reject) => setTimeout(
9605
- () => reject(new Error("Transaction confirmation timeout")),
9606
- this.transactionTimeoutMs
9607
- )
9608
- )
9609
- ]);
9633
+ const receipt = senderIsContractWallet ? await this.awaitPositionCreatedByPkpId(
9634
+ positionManager,
9635
+ pkpIdBytes32,
9636
+ searchFromBlock,
9637
+ confirmations
9638
+ ) : await this.awaitReceiptByHash(tx, confirmations);
9610
9639
  if (!receipt)
9611
9640
  throw new Error("Transaction was not mined (receipt is null)");
9612
9641
  if (this.config.debug && receipt.logs) {
@@ -9740,6 +9769,81 @@ var LoanCreator = class {
9740
9769
  }
9741
9770
  return 3;
9742
9771
  }
9772
+ /**
9773
+ * EOA path: the returned hash is the L1 hash, so `tx.wait` is exact.
9774
+ *
9775
+ * The timer is cleared on settle — an uncleared one keeps the process alive for the
9776
+ * full timeout after a fast confirmation, which in Node holds the event loop open.
9777
+ */
9778
+ async awaitReceiptByHash(tx, confirmations) {
9779
+ let timer;
9780
+ try {
9781
+ return await Promise.race([
9782
+ tx.wait(confirmations),
9783
+ new Promise((_, reject) => {
9784
+ timer = setTimeout(
9785
+ () => reject(new Error("Transaction confirmation timeout")),
9786
+ this.transactionTimeoutMs
9787
+ );
9788
+ })
9789
+ ]);
9790
+ } finally {
9791
+ if (timer)
9792
+ clearTimeout(timer);
9793
+ }
9794
+ }
9795
+ /**
9796
+ * Contract-wallet path: find this position by its `PositionCreated` log.
9797
+ *
9798
+ * `pkpId` is minted fresh per attempt and is the event's second indexed argument, so a
9799
+ * topic filter on it identifies exactly this position without reference to any transaction
9800
+ * hash. The filter deliberately leaves topic0 unconstrained: two `PositionCreated` ABIs are
9801
+ * in circulation (see `extractPositionId`) and matching on the freshly-minted pkpId alone,
9802
+ * scoped to this contract and to blocks at or after the send, is already unambiguous.
9803
+ *
9804
+ * The same confirmation depth as the EOA path is applied to the log's block, for the reason
9805
+ * given at the call site: `positionId` does not survive a reorg unchanged.
9806
+ */
9807
+ async awaitPositionCreatedByPkpId(positionManager, pkpIdBytes32, fromBlock, confirmations) {
9808
+ const provider = this.config.contractManager.getProvider();
9809
+ const contractAddress = await positionManager.getAddress();
9810
+ const deadline = Date.now() + this.transactionTimeoutMs;
9811
+ while (Date.now() < deadline) {
9812
+ const logs = await provider.getLogs({
9813
+ address: contractAddress,
9814
+ topics: [null, null, pkpIdBytes32],
9815
+ fromBlock,
9816
+ toBlock: "latest"
9817
+ });
9818
+ if (logs.length > 1) {
9819
+ throw new Error(
9820
+ `Expected at most one PositionCreated log for pkpId ${pkpIdBytes32}, found ${logs.length}`
9821
+ );
9822
+ }
9823
+ const positionLog = logs[0];
9824
+ if (positionLog) {
9825
+ const head = await provider.getBlockNumber();
9826
+ const depth = head - positionLog.blockNumber + 1;
9827
+ if (depth >= confirmations) {
9828
+ const receipt = await provider.getTransactionReceipt(
9829
+ positionLog.transactionHash
9830
+ );
9831
+ if (!receipt) {
9832
+ throw new Error(
9833
+ `PositionCreated log for pkpId ${pkpIdBytes32} has no receipt at ${positionLog.transactionHash} \u2014 the block was reorged out mid-confirmation.`
9834
+ );
9835
+ }
9836
+ return receipt;
9837
+ }
9838
+ }
9839
+ await new Promise(
9840
+ (resolve) => setTimeout(resolve, CONTRACT_WALLET_POLL_INTERVAL_MS)
9841
+ );
9842
+ }
9843
+ throw new Error(
9844
+ `Timed out after ${this.transactionTimeoutMs}ms waiting for the PositionCreated log for pkpId ${pkpIdBytes32}. The wallet may not have executed the transaction yet; the position must be rediscovered by pkpId before it is used.`
9845
+ );
9846
+ }
9743
9847
  /**
9744
9848
  * Re-read the receipt after confirmations and re-derive `positionId`, so a reorg
9745
9849
  * cannot leave the caller holding an id that never survived to the canonical chain.
@@ -18772,7 +18876,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
18772
18876
  litNetwork: config.litNetwork,
18773
18877
  loanCreationValidatorVersion: config.validators?.loanCreation ?? 1,
18774
18878
  ethRpcUrl: config.ethRpcUrl,
18775
- temperSignaturesTest: config.temperSignaturesTest
18879
+ temperSignaturesTest: config.temperSignaturesTest,
18880
+ onPkpCreated: config.onPkpCreated
18776
18881
  });
18777
18882
  if (!loanCreatorResult.success) {
18778
18883
  throw new Error(
package/dist/index.mjs CHANGED
@@ -9099,6 +9099,23 @@ function createPKPManager(config) {
9099
9099
  // src/modules/loan/loan-creator.module.ts
9100
9100
  import { Contract as Contract3, Interface as Interface2, zeroPadValue as zeroPadValue4, toBeHex, SigningKey } from "ethers";
9101
9101
  var import_dh_lit_actions = __toESM(require_pkg_src());
9102
+
9103
+ // src/utils/contract-wallet.utils.ts
9104
+ var contractWalletCache = /* @__PURE__ */ new Map();
9105
+ async function isContractWallet(provider, address) {
9106
+ const { chainId } = await provider.getNetwork();
9107
+ const cacheKey = `${chainId}:${address.toLowerCase()}`;
9108
+ const cached = contractWalletCache.get(cacheKey);
9109
+ if (cached !== void 0)
9110
+ return cached;
9111
+ const code = await provider.getCode(address);
9112
+ const isContract = code !== "0x";
9113
+ contractWalletCache.set(cacheKey, isContract);
9114
+ return isContract;
9115
+ }
9116
+
9117
+ // src/modules/loan/loan-creator.module.ts
9118
+ var CONTRACT_WALLET_POLL_INTERVAL_MS = 4e3;
9102
9119
  var LoanCreator = class {
9103
9120
  config;
9104
9121
  transactionTimeoutMs;
@@ -9322,6 +9339,11 @@ var LoanCreator = class {
9322
9339
  validatorSignature: validatorSignature.substring(0, 20) + "..."
9323
9340
  });
9324
9341
  }
9342
+ this.config.onPkpCreated?.({
9343
+ tokenId: pkpData.tokenId,
9344
+ ethAddress: pkpData.ethAddress,
9345
+ publicKey: pkpData.publicKey
9346
+ });
9325
9347
  const addressesResult = await this.deriveBitcoinAddresses(
9326
9348
  pkpData.publicKey
9327
9349
  );
@@ -9494,6 +9516,12 @@ var LoanCreator = class {
9494
9516
  error: encErr instanceof Error ? encErr.message : String(encErr)
9495
9517
  });
9496
9518
  }
9519
+ const provider = this.config.contractManager.getProvider();
9520
+ const senderIsContractWallet = await isContractWallet(
9521
+ provider,
9522
+ await signerResult.value.getAddress()
9523
+ );
9524
+ const searchFromBlock = senderIsContractWallet ? await provider.getBlockNumber() : 0;
9497
9525
  const tx = await positionManager.createPosition(
9498
9526
  pkpIdBytes32,
9499
9527
  validatorSignature,
@@ -9515,19 +9543,20 @@ var LoanCreator = class {
9515
9543
  }
9516
9544
  if (this.config.debug) {
9517
9545
  log.info("\u23F3 Waiting for transaction confirmation", {
9518
- hash: tx.hash
9546
+ // For a contract wallet this is the wallet's own id, not an L1 hash — which is
9547
+ // exactly why the confirmation below does not wait on it.
9548
+ hash: tx.hash,
9549
+ senderIsContractWallet,
9550
+ searchFromBlock: senderIsContractWallet ? searchFromBlock : void 0
9519
9551
  });
9520
9552
  }
9521
9553
  const confirmations = await this.createPositionConfirmations();
9522
- const receipt = await Promise.race([
9523
- tx.wait(confirmations),
9524
- new Promise(
9525
- (_, reject) => setTimeout(
9526
- () => reject(new Error("Transaction confirmation timeout")),
9527
- this.transactionTimeoutMs
9528
- )
9529
- )
9530
- ]);
9554
+ const receipt = senderIsContractWallet ? await this.awaitPositionCreatedByPkpId(
9555
+ positionManager,
9556
+ pkpIdBytes32,
9557
+ searchFromBlock,
9558
+ confirmations
9559
+ ) : await this.awaitReceiptByHash(tx, confirmations);
9531
9560
  if (!receipt)
9532
9561
  throw new Error("Transaction was not mined (receipt is null)");
9533
9562
  if (this.config.debug && receipt.logs) {
@@ -9661,6 +9690,81 @@ var LoanCreator = class {
9661
9690
  }
9662
9691
  return 3;
9663
9692
  }
9693
+ /**
9694
+ * EOA path: the returned hash is the L1 hash, so `tx.wait` is exact.
9695
+ *
9696
+ * The timer is cleared on settle — an uncleared one keeps the process alive for the
9697
+ * full timeout after a fast confirmation, which in Node holds the event loop open.
9698
+ */
9699
+ async awaitReceiptByHash(tx, confirmations) {
9700
+ let timer;
9701
+ try {
9702
+ return await Promise.race([
9703
+ tx.wait(confirmations),
9704
+ new Promise((_, reject) => {
9705
+ timer = setTimeout(
9706
+ () => reject(new Error("Transaction confirmation timeout")),
9707
+ this.transactionTimeoutMs
9708
+ );
9709
+ })
9710
+ ]);
9711
+ } finally {
9712
+ if (timer)
9713
+ clearTimeout(timer);
9714
+ }
9715
+ }
9716
+ /**
9717
+ * Contract-wallet path: find this position by its `PositionCreated` log.
9718
+ *
9719
+ * `pkpId` is minted fresh per attempt and is the event's second indexed argument, so a
9720
+ * topic filter on it identifies exactly this position without reference to any transaction
9721
+ * hash. The filter deliberately leaves topic0 unconstrained: two `PositionCreated` ABIs are
9722
+ * in circulation (see `extractPositionId`) and matching on the freshly-minted pkpId alone,
9723
+ * scoped to this contract and to blocks at or after the send, is already unambiguous.
9724
+ *
9725
+ * The same confirmation depth as the EOA path is applied to the log's block, for the reason
9726
+ * given at the call site: `positionId` does not survive a reorg unchanged.
9727
+ */
9728
+ async awaitPositionCreatedByPkpId(positionManager, pkpIdBytes32, fromBlock, confirmations) {
9729
+ const provider = this.config.contractManager.getProvider();
9730
+ const contractAddress = await positionManager.getAddress();
9731
+ const deadline = Date.now() + this.transactionTimeoutMs;
9732
+ while (Date.now() < deadline) {
9733
+ const logs = await provider.getLogs({
9734
+ address: contractAddress,
9735
+ topics: [null, null, pkpIdBytes32],
9736
+ fromBlock,
9737
+ toBlock: "latest"
9738
+ });
9739
+ if (logs.length > 1) {
9740
+ throw new Error(
9741
+ `Expected at most one PositionCreated log for pkpId ${pkpIdBytes32}, found ${logs.length}`
9742
+ );
9743
+ }
9744
+ const positionLog = logs[0];
9745
+ if (positionLog) {
9746
+ const head = await provider.getBlockNumber();
9747
+ const depth = head - positionLog.blockNumber + 1;
9748
+ if (depth >= confirmations) {
9749
+ const receipt = await provider.getTransactionReceipt(
9750
+ positionLog.transactionHash
9751
+ );
9752
+ if (!receipt) {
9753
+ throw new Error(
9754
+ `PositionCreated log for pkpId ${pkpIdBytes32} has no receipt at ${positionLog.transactionHash} \u2014 the block was reorged out mid-confirmation.`
9755
+ );
9756
+ }
9757
+ return receipt;
9758
+ }
9759
+ }
9760
+ await new Promise(
9761
+ (resolve) => setTimeout(resolve, CONTRACT_WALLET_POLL_INTERVAL_MS)
9762
+ );
9763
+ }
9764
+ throw new Error(
9765
+ `Timed out after ${this.transactionTimeoutMs}ms waiting for the PositionCreated log for pkpId ${pkpIdBytes32}. The wallet may not have executed the transaction yet; the position must be rediscovered by pkpId before it is used.`
9766
+ );
9767
+ }
9664
9768
  /**
9665
9769
  * Re-read the receipt after confirmations and re-derive `positionId`, so a reorg
9666
9770
  * cannot leave the caller holding an id that never survived to the canonical chain.
@@ -18701,7 +18805,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
18701
18805
  litNetwork: config.litNetwork,
18702
18806
  loanCreationValidatorVersion: config.validators?.loanCreation ?? 1,
18703
18807
  ethRpcUrl: config.ethRpcUrl,
18704
- temperSignaturesTest: config.temperSignaturesTest
18808
+ temperSignaturesTest: config.temperSignaturesTest,
18809
+ onPkpCreated: config.onPkpCreated
18705
18810
  });
18706
18811
  if (!loanCreatorResult.success) {
18707
18812
  throw new Error(
@@ -102,6 +102,24 @@ interface BaseSDKConfig {
102
102
  * contract wallet can leave that request open for minutes.
103
103
  */
104
104
  onSessionSignatureResolved?: (ok: boolean) => void;
105
+ /**
106
+ * Fires during loan creation the moment the PKP has been minted and validated,
107
+ * before the `createPosition` transaction is submitted.
108
+ *
109
+ * The PKP is minted fresh per attempt, so `tokenId` is a unique, send-independent
110
+ * handle on the position that transaction is about to create — it is the only
111
+ * identifier a caller can hold *before* a hash exists. UIs use it to recognise
112
+ * their own position in an indexed feed without depending on the transaction
113
+ * hash, which a contract wallet does not return in a usable form.
114
+ *
115
+ * Note `tokenId` is a decimal string; the on-chain and subgraph `pkpId` is the
116
+ * same value as 32-byte hex. Normalise before comparing.
117
+ */
118
+ onPkpCreated?: (pkp: {
119
+ tokenId: string;
120
+ ethAddress: string;
121
+ publicKey: string;
122
+ }) => void;
105
123
  ethRpcUrl?: string;
106
124
  chainId?: number;
107
125
  networkOverride?: {
@@ -61,6 +61,15 @@ export interface LoanCreatorConfig {
61
61
  ethRpcUrl?: string;
62
62
  /** Security testing: Temper signatures to test contract validation */
63
63
  temperSignaturesTest?: boolean;
64
+ /**
65
+ * Fires once the PKP is minted and validated, before `createPosition` is submitted.
66
+ * See `SDKConfig.onPkpCreated` for why this exists and for the tokenId encoding caveat.
67
+ */
68
+ onPkpCreated?: (pkp: {
69
+ tokenId: string;
70
+ ethAddress: string;
71
+ publicKey: string;
72
+ }) => void;
64
73
  }
65
74
  /**
66
75
  * Loan creation audit trail
@@ -133,6 +142,26 @@ export declare class LoanCreator {
133
142
  * Override with `createPositionConfirmations` when a chain needs more.
134
143
  */
135
144
  private createPositionConfirmations;
145
+ /**
146
+ * EOA path: the returned hash is the L1 hash, so `tx.wait` is exact.
147
+ *
148
+ * The timer is cleared on settle — an uncleared one keeps the process alive for the
149
+ * full timeout after a fast confirmation, which in Node holds the event loop open.
150
+ */
151
+ private awaitReceiptByHash;
152
+ /**
153
+ * Contract-wallet path: find this position by its `PositionCreated` log.
154
+ *
155
+ * `pkpId` is minted fresh per attempt and is the event's second indexed argument, so a
156
+ * topic filter on it identifies exactly this position without reference to any transaction
157
+ * hash. The filter deliberately leaves topic0 unconstrained: two `PositionCreated` ABIs are
158
+ * in circulation (see `extractPositionId`) and matching on the freshly-minted pkpId alone,
159
+ * scoped to this contract and to blocks at or after the send, is already unambiguous.
160
+ *
161
+ * The same confirmation depth as the EOA path is applied to the log's block, for the reason
162
+ * given at the call site: `positionId` does not survive a reorg unchanged.
163
+ */
164
+ private awaitPositionCreatedByPkpId;
136
165
  /**
137
166
  * Re-read the receipt after confirmations and re-derive `positionId`, so a reorg
138
167
  * cannot leave the caller holding an id that never survived to the canonical chain.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Contract-wallet detection.
3
+ *
4
+ * A Safe (or any smart-account) returns its OWN transaction identifier from
5
+ * `eth_sendTransaction` — the safeTxHash — not an L1 transaction hash. Anything that
6
+ * waits on that identifier waits forever, because it never lands on chain. Callers
7
+ * that need to confirm a transaction must therefore know which kind of sender they
8
+ * are dealing with and confirm by some other means.
9
+ *
10
+ * Contract-ness is settled by asking the chain for code at the address, which is the
11
+ * same test the servers use for the EIP-1271 path — not the wallet name, which is
12
+ * whatever the connector chose to call itself.
13
+ */
14
+ import type { Provider } from "ethers";
15
+ /**
16
+ * True when `address` has code on the chain behind `provider`.
17
+ *
18
+ * Deliberately propagates provider errors rather than assuming EOA: a failed `getCode`
19
+ * means the provider is unreachable, and guessing "EOA" there would send the caller
20
+ * back into a wait that cannot complete.
21
+ */
22
+ export declare function isContractWallet(provider: Provider, address: string): Promise<boolean>;
23
+ /** Test seam — clears memoised detections. */
24
+ export declare function resetContractWalletCache(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.314",
3
+ "version": "0.0.315",
4
4
  "description": "TypeScript SDK for Diamond Hands Protocol - Bitcoin-backed lending with LIT Protocol PKPs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",