Skip to content

Commit 4f7e530

Browse files
committed
fix(cashu): apply P2PK conditions in CDK send and improve locked-token error
1 parent 0b057ff commit 4f7e530

File tree

3 files changed

+96
-77
lines changed

3 files changed

+96
-77
lines changed

android/app/src/main/java/com/zeus/cashudevkit/CashuDevKitModule.kt

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class CashuDevKitModule(private val reactContext: ReactApplicationContext) :
167167
return JSONObject().apply {
168168
put("encoded", token.encode())
169169
put("value", token.value().value)
170-
put("mint_url", token.mintUrl().toString())
170+
put("mint_url", token.mintUrl().url)
171171
put("memo", token.memo() ?: "")
172172
put("unit", token.unit()?.let { currencyUnitToString(it) } ?: "sat")
173173
}
@@ -866,13 +866,48 @@ class CashuDevKitModule(private val reactContext: ReactApplicationContext) :
866866
val amt = Amount(amount.toLong().toULong())
867867

868868
// Parse options if provided
869-
val includeFee = optionsJson?.let {
870-
JSONObject(it).optBoolean("include_fee", false)
871-
} ?: false
869+
var includeFee = false
870+
var conditions: SpendingConditions? = null
871+
if (optionsJson != null) {
872+
val parsed = JSONObject(optionsJson)
873+
includeFee = parsed.optBoolean("include_fee", false)
874+
875+
// Parse spending conditions (P2PK) if provided
876+
if (parsed.has("conditions") && !parsed.isNull("conditions")) {
877+
val cond = parsed.getJSONObject("conditions")
878+
val kind = cond.optString("kind")
879+
if (kind == "P2PK" && cond.has("data")) {
880+
val data = cond.getJSONObject("data")
881+
val pubkeyHex = data.getString("pubkey")
882+
val locktime =
883+
if (data.has("locktime") && !data.isNull("locktime"))
884+
data.getLong("locktime").toULong()
885+
else null
886+
val refundKeysJson = data.optJSONArray("refund_keys")
887+
val refundKeys = refundKeysJson?.let { arr ->
888+
(0 until arr.length()).mapNotNull { i ->
889+
try { arr.getString(i) } catch (e: Exception) { null }
890+
}
891+
}
892+
893+
conditions = SpendingConditions.P2pk(
894+
pubkey = pubkeyHex,
895+
conditions = Conditions(
896+
locktime = locktime ?: 0UL,
897+
pubkeys = emptyList(),
898+
refundKeys = refundKeys ?: emptyList(),
899+
numSigs = 0UL,
900+
sigFlag = 0.toUByte(),
901+
numSigsRefund = 0UL
902+
)
903+
)
904+
}
905+
}
906+
}
872907

873908
val innerSendOptions = SendOptions(
874909
memo = null,
875-
conditions = null,
910+
conditions = conditions,
876911
amountSplitTarget = SplitTarget.None,
877912
sendKind = SendKind.OnlineExact,
878913
includeFee = includeFee,

ios/CashuDevKit/CashuDevKitModule.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,17 +769,37 @@ class CashuDevKitModule: RCTEventEmitter {
769769

770770
// Parse options if provided
771771
var includeFee = false
772+
var spendingConditions: SpendingConditions? = nil
772773
if let json = optionsJson,
773774
let data = json.data(using: .utf8),
774775
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
775776
if let fee = parsed["include_fee"] as? Bool {
776777
includeFee = fee
777778
}
779+
780+
// Parse spending conditions (P2PK) if provided
781+
if let cond = parsed["conditions"] as? [String: Any],
782+
let kind = cond["kind"] as? String,
783+
kind == "P2PK",
784+
let condData = cond["data"] as? [String: Any],
785+
let pubkey = condData["pubkey"] as? String {
786+
let locktime = condData["locktime"] as? UInt64
787+
let refundKeys = condData["refund_keys"] as? [String] ?? []
788+
let inner = Conditions(
789+
locktime: locktime,
790+
pubkeys: [],
791+
refundKeys: refundKeys,
792+
numSigs: nil,
793+
sigFlag: 0,
794+
numSigsRefund: nil
795+
)
796+
spendingConditions = .p2pk(pubkey: pubkey, conditions: inner)
797+
}
778798
}
779799

780800
let innerSendOptions = SendOptions(
781801
memo: nil,
782-
conditions: nil,
802+
conditions: spendingConditions,
783803
amountSplitTarget: .none,
784804
sendKind: .onlineExact,
785805
includeFee: includeFee,

stores/CashuStore.ts

Lines changed: 35 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import CashuDevKit, {
1111
CDKMeltQuote,
1212
CDKMelted,
1313
CDKToken,
14-
CDKSpendingConditions
14+
CDKSpendingConditions,
15+
CDKP2PKCondition
1516
} from '../cashu-cdk';
1617

1718
import { LNURLWithdrawParams } from 'js-lnurl';
@@ -723,12 +724,15 @@ export default class CashuStore {
723724

724725
let conditions: CDKSpendingConditions | undefined;
725726
if (p2pkPubkey) {
727+
const conditionData: CDKP2PKCondition = {
728+
pubkey: p2pkPubkey
729+
};
730+
if (locktime) {
731+
conditionData.locktime = locktime;
732+
}
726733
conditions = {
727734
kind: 'P2PK',
728-
data: {
729-
pubkey: p2pkPubkey,
730-
locktime
731-
}
735+
data: conditionData
732736
};
733737
}
734738

@@ -1117,47 +1121,6 @@ export default class CashuStore {
11171121
return this.cashuWallets;
11181122
};
11191123

1120-
private getSeed = () => {
1121-
if (this.seed) return this.seed;
1122-
1123-
if (true || this.seedVersion === 'v2-bip39') {
1124-
const lndSeedPhrase = this.settingsStore.seedPhrase;
1125-
const mnemonic = lndSeedPhrase.join(' ');
1126-
1127-
const seedFromMnemonic = bip39scure.mnemonicToSeedSync(mnemonic);
1128-
// only need 16 bytes for a 12 word phrase
1129-
const entropy = seedFromMnemonic.slice(48, 64);
1130-
1131-
const cashuSeedPhrase = bip39scure.entropyToMnemonic(
1132-
entropy,
1133-
BIP39_WORD_LIST
1134-
);
1135-
const seedPhrase = cashuSeedPhrase.split(' ');
1136-
1137-
Storage.setItem(
1138-
`${this.getLndDir()}-cashu-seed-phrase`,
1139-
seedPhrase
1140-
);
1141-
this.seedPhrase = seedPhrase;
1142-
1143-
const seed = bip39scure.mnemonicToSeedSync(cashuSeedPhrase);
1144-
1145-
Storage.setItem(
1146-
`${this.getLndDir()}-cashu-seed`,
1147-
Base64Utils.bytesToBase64(seed)
1148-
);
1149-
this.seed = seed;
1150-
return this.seed;
1151-
}
1152-
1153-
// v1
1154-
const seedPhrase = this.settingsStore.seedPhrase;
1155-
const mnemonic = seedPhrase.join(' ');
1156-
const seed = bip39.mnemonicToSeedSync(mnemonic);
1157-
this.seed = new Uint8Array(seed.slice(32, 64)); // limit to 32 bytes
1158-
return this.seed;
1159-
};
1160-
11611124
setTotalBalance = async (newTotalBalanceSats: number) => {
11621125
const previousBalance = this.totalBalanceSats || 0;
11631126

@@ -2189,18 +2152,12 @@ export default class CashuStore {
21892152
noLsp: true
21902153
};
21912154

2192-
// First receive the token via CDK
2193-
const isLocked = CashuUtils.isTokenP2PKLocked(decoded);
2194-
let signingKey: string | undefined;
2195-
if (isLocked) {
2196-
const bip39seed = this.getSeed();
2197-
if (bip39seed) {
2198-
signingKey = Base64Utils.base64ToHex(
2199-
Base64Utils.bytesToBase64(bip39seed.slice(0, 32))
2200-
);
2201-
}
2202-
}
2203-
await this.receiveTokenCDK(encodedToken, signingKey);
2155+
// Providing our Cashu secret key; CDK will only use it if the token is P2PK-locked.
2156+
const signingKey = this.deriveCashuSecretKey();
2157+
await this.receiveTokenCDK(
2158+
encodedToken,
2159+
signingKey || undefined
2160+
);
22042161
await this.syncCDKBalances();
22052162

22062163
// Create invoice for sweeping
@@ -2244,19 +2201,12 @@ export default class CashuStore {
22442201
await this.syncCDKBalances(true); // Include transactions for activity
22452202
} else {
22462203
// Regular receive via CDK
2247-
const isLocked = CashuUtils.isTokenP2PKLocked(decoded);
2248-
let signingKey: string | undefined;
2249-
2250-
if (isLocked) {
2251-
const bip39seed = this.getSeed();
2252-
if (bip39seed) {
2253-
signingKey = Base64Utils.base64ToHex(
2254-
Base64Utils.bytesToBase64(bip39seed.slice(0, 32))
2255-
);
2256-
}
2257-
}
2258-
2259-
await this.receiveTokenCDK(encodedToken, signingKey);
2204+
// Providing our Cashu secret key; CDK will only use it if the token is P2PK-locked.
2205+
const signingKey = this.deriveCashuSecretKey();
2206+
await this.receiveTokenCDK(
2207+
encodedToken,
2208+
signingKey || undefined
2209+
);
22602210

22612211
// Record received token activity
22622212
this.receivedTokens?.push(
@@ -2289,6 +2239,20 @@ export default class CashuStore {
22892239
errorMessage: localeString('stores.CashuStore.alreadySpent')
22902240
};
22912241
}
2242+
if (
2243+
e?.message?.includes('Witness is missing for p2pk signature') ||
2244+
(typeof e?.message === 'string' &&
2245+
e.message.toLowerCase().includes('witness is missing') &&
2246+
e.message.toLowerCase().includes('p2pk'))
2247+
) {
2248+
this.loading = false;
2249+
return {
2250+
success: false,
2251+
errorMessage: localeString(
2252+
'stores.CashuStore.claimError.lockedToWallet'
2253+
)
2254+
};
2255+
}
22922256
this.loading = false;
22932257
return {
22942258
success: false,

0 commit comments

Comments
 (0)