passkey-kit 0.2.1 → 0.2.3
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/package.json +1 -1
- package/src/kit.ts +106 -91
- package/types/kit.d.ts +12 -4
package/package.json
CHANGED
package/src/kit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Client as PasskeyClient } from 'passkey-kit-sdk'
|
|
2
2
|
import { Client as FactoryClient, networks } from 'passkey-factory-sdk'
|
|
3
|
-
import { Address, Networks, StrKey, hash, xdr, Transaction, SorobanRpc, Operation, scValToNative, TransactionBuilder } from '@stellar/stellar-sdk'
|
|
3
|
+
import { Address, Networks, StrKey, hash, xdr, Transaction, SorobanRpc, Operation, scValToNative, TransactionBuilder, authorizeEntry } from '@stellar/stellar-sdk'
|
|
4
4
|
import { bufToBigint, bigintToBuf } from 'bigint-conversion'
|
|
5
5
|
import base64url from 'base64url'
|
|
6
6
|
import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
|
|
@@ -12,7 +12,6 @@ import { PasskeyBase } from './base'
|
|
|
12
12
|
- Clean up these params and the interface as a whole
|
|
13
13
|
Might put wallet activities and maybe factory as well into the root of the class vs buried inside this.wallet and this.factory
|
|
14
14
|
@Later
|
|
15
|
-
|
|
16
15
|
- Right now publicKey can mean a Stellar public key or a passkey public key, there should be a noted difference
|
|
17
16
|
*/
|
|
18
17
|
|
|
@@ -26,32 +25,27 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
26
25
|
public rpc: SorobanRpc.Server
|
|
27
26
|
public factoryContractId: string = networks.testnet.contractId
|
|
28
27
|
|
|
29
|
-
/*
|
|
28
|
+
/* NOTE
|
|
30
29
|
- Consider adding the ability to pass in a keyId and maybe even a contractId in order to preconnect to a wallet
|
|
31
30
|
If just a keyId call `connectWallet` in order to get the contractId
|
|
32
31
|
If both keyId and contractId are passed in then we can skip the connectWallet call (though we won't get the sudoKeyId in that case)
|
|
33
32
|
We don't strictly _need_ this as a dev can just call `connectWallet` after class instantiation but it might be a nice convenience
|
|
34
|
-
|
|
33
|
+
`connectWallet` is async making it tricky to know when the wallet is "ready". Thus I think it's better for `connectWallet` to be called externally
|
|
34
|
+
@No
|
|
35
35
|
*/
|
|
36
|
-
|
|
37
36
|
constructor(options: {
|
|
38
|
-
networkPassphrase: Networks,
|
|
39
37
|
rpcUrl: string,
|
|
40
38
|
launchtubeUrl?: string,
|
|
41
39
|
launchtubeJwt?: string,
|
|
42
|
-
/* TODO
|
|
43
|
-
- Maybe remove this? The factory should likely be baked in a bit more tightly
|
|
44
|
-
On the other hand once we have a standard interface the factory interface only uses the `deploy` method right now inside the interface
|
|
45
|
-
@No
|
|
46
|
-
*/
|
|
47
40
|
factoryContractId?: string,
|
|
41
|
+
networkPassphrase: Networks,
|
|
48
42
|
}) {
|
|
49
43
|
const {
|
|
50
|
-
networkPassphrase,
|
|
51
44
|
rpcUrl,
|
|
52
45
|
launchtubeUrl,
|
|
53
46
|
launchtubeJwt,
|
|
54
|
-
factoryContractId
|
|
47
|
+
factoryContractId,
|
|
48
|
+
networkPassphrase,
|
|
55
49
|
} = options
|
|
56
50
|
|
|
57
51
|
super({
|
|
@@ -59,13 +53,12 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
59
53
|
launchtubeJwt,
|
|
60
54
|
})
|
|
61
55
|
|
|
62
|
-
this.networkPassphrase = networkPassphrase
|
|
63
|
-
this.rpcUrl = rpcUrl
|
|
64
|
-
this.rpc = new SorobanRpc.Server(rpcUrl)
|
|
65
|
-
|
|
66
56
|
if (factoryContractId)
|
|
67
57
|
this.factoryContractId = factoryContractId
|
|
68
58
|
|
|
59
|
+
this.rpcUrl = rpcUrl
|
|
60
|
+
this.rpc = new SorobanRpc.Server(rpcUrl)
|
|
61
|
+
this.networkPassphrase = networkPassphrase
|
|
69
62
|
this.factory = new FactoryClient({
|
|
70
63
|
contractId: this.factoryContractId,
|
|
71
64
|
networkPassphrase,
|
|
@@ -139,34 +132,21 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
139
132
|
}
|
|
140
133
|
}
|
|
141
134
|
|
|
142
|
-
public async connectWallet(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Maybe not as we wouldn't have a keyId which could have interesting consequences
|
|
146
|
-
Also not sure what the use case would be for this where keyId wouldn't also be possible and better
|
|
147
|
-
@No
|
|
148
|
-
*/
|
|
149
|
-
|
|
150
|
-
// @ts-ignore
|
|
151
|
-
// https://github.com/stellar/js-stellar-base/issues/750
|
|
152
|
-
// if (id && StrKey.isValidContract(id)) {
|
|
153
|
-
|
|
154
|
-
// } else {
|
|
155
|
-
|
|
156
|
-
// }
|
|
157
|
-
|
|
158
|
-
const startAuthenticationResponse = id
|
|
159
|
-
? { id }
|
|
160
|
-
: await startAuthentication({
|
|
135
|
+
public async connectWallet(keyId?: string) {
|
|
136
|
+
if (!keyId) {
|
|
137
|
+
const { id } = await startAuthentication({
|
|
161
138
|
challenge: base64url("stellaristhebetterblockchain"),
|
|
162
139
|
// rpId: undefined,
|
|
163
140
|
userVerification: "discouraged",
|
|
164
141
|
});
|
|
165
142
|
|
|
143
|
+
keyId = id
|
|
144
|
+
}
|
|
145
|
+
|
|
166
146
|
if (!this.keyId)
|
|
167
|
-
this.keyId =
|
|
147
|
+
this.keyId = keyId
|
|
168
148
|
|
|
169
|
-
const keyIdBuffer = base64url.toBuffer(
|
|
149
|
+
const keyIdBuffer = base64url.toBuffer(keyId)
|
|
170
150
|
|
|
171
151
|
// NOTE might not need this for derivation as all signers are stored in the factory and we can use that lookup as both primary and secondary
|
|
172
152
|
let contractId = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
|
|
@@ -181,14 +161,16 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
181
161
|
})
|
|
182
162
|
).toXDR()));
|
|
183
163
|
|
|
164
|
+
let contractData: SorobanRpc.Api.LedgerEntryResult | undefined
|
|
165
|
+
|
|
184
166
|
// attempt passkey id derivation
|
|
185
167
|
try {
|
|
186
|
-
await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
|
|
168
|
+
contractData = await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
|
|
187
169
|
}
|
|
188
170
|
// if that fails look up from the factory mapper
|
|
189
171
|
catch {
|
|
190
|
-
|
|
191
|
-
contractId = scValToNative(val.contractData().val())
|
|
172
|
+
contractData = await this.rpc.getContractData(this.factoryContractId, xdr.ScVal.scvBytes(keyIdBuffer))
|
|
173
|
+
contractId = scValToNative(contractData.val.contractData().val())
|
|
192
174
|
}
|
|
193
175
|
|
|
194
176
|
this.wallet = new PasskeyClient({
|
|
@@ -197,8 +179,8 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
197
179
|
rpcUrl: this.rpcUrl
|
|
198
180
|
})
|
|
199
181
|
|
|
200
|
-
// get and set the sudo signer
|
|
201
|
-
await this.getData()
|
|
182
|
+
// get and set the sudo signer (passing in `contractData` from the `getContractData` call above to avoid dupe requests)
|
|
183
|
+
await this.getData(contractData)
|
|
202
184
|
|
|
203
185
|
return {
|
|
204
186
|
keyId: keyIdBuffer,
|
|
@@ -206,13 +188,8 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
206
188
|
}
|
|
207
189
|
}
|
|
208
190
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Perhaps not but maybe the js-sdk's `authorizeEntry` or `authorizeInvocation`
|
|
212
|
-
https://stellar.github.io/js-stellar-sdk/global.html#authorizeInvocation
|
|
213
|
-
*/
|
|
214
|
-
public async sign(
|
|
215
|
-
txn: Transaction | string,
|
|
191
|
+
public async signAuthEntry(
|
|
192
|
+
entry: xdr.SorobanAuthorizationEntry,
|
|
216
193
|
options?: {
|
|
217
194
|
keyId?: 'any' | 'sudo' | string | Uint8Array
|
|
218
195
|
ledgersToLive?: number
|
|
@@ -221,46 +198,29 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
221
198
|
// Default mirrors DEFAULT_TIMEOUT (currently 5 minutes) https://github.com/stellar/js-stellar-sdk/blob/master/src/contract/utils.ts#L7
|
|
222
199
|
let { keyId, ledgersToLive = 60 } = options || {}
|
|
223
200
|
|
|
224
|
-
// Hack to ensure we don't stack fees when simulating and assembling multiple times
|
|
225
|
-
txn = TransactionBuilder.cloneFrom(new Transaction(
|
|
226
|
-
typeof txn === 'string'
|
|
227
|
-
? txn
|
|
228
|
-
: txn.toXDR(),
|
|
229
|
-
this.networkPassphrase
|
|
230
|
-
), { fee: '0' }).build()
|
|
231
|
-
|
|
232
|
-
// NOTE hard coded to sign only Soroban transactions and only and always the first auth
|
|
233
|
-
/* TODO !!!
|
|
234
|
-
- Switch to support signing multiple and specific auth entries
|
|
235
|
-
Possibly pass in auth entry vs the entire txn
|
|
236
|
-
Probably a bad idea as it would add complexity to the user side to reassemble the signed entries
|
|
237
|
-
I think we can probably look at the list of auth entries and assume which ones we want to sign
|
|
238
|
-
*/
|
|
239
|
-
const op = txn.operations[0] as Operation.InvokeHostFunction
|
|
240
|
-
const auth = op.auth![0]
|
|
241
201
|
const lastLedger = await this.rpc.getLatestLedger().then(({ sequence }) => sequence)
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
).toXDR()
|
|
202
|
+
const credentials = entry.credentials().address();
|
|
203
|
+
const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
204
|
+
new xdr.HashIdPreimageSorobanAuthorization({
|
|
205
|
+
networkId: hash(Buffer.from(this.networkPassphrase)),
|
|
206
|
+
nonce: credentials.nonce(),
|
|
207
|
+
signatureExpirationLedger: lastLedger + ledgersToLive,
|
|
208
|
+
invocation: entry.rootInvocation()
|
|
209
|
+
})
|
|
251
210
|
)
|
|
211
|
+
const payload = hash(preimage.toXDR())
|
|
252
212
|
|
|
253
213
|
const authenticationResponse = await startAuthentication(
|
|
254
214
|
keyId === 'any'
|
|
255
215
|
|| (keyId === 'sudo' && !this.sudoKeyId)
|
|
256
216
|
|| (!keyId && !this.keyId)
|
|
257
217
|
? {
|
|
258
|
-
challenge: base64url(
|
|
218
|
+
challenge: base64url(payload),
|
|
259
219
|
// rpId: undefined,
|
|
260
220
|
userVerification: "discouraged",
|
|
261
221
|
}
|
|
262
222
|
: {
|
|
263
|
-
challenge: base64url(
|
|
223
|
+
challenge: base64url(payload),
|
|
264
224
|
// rpId: undefined,
|
|
265
225
|
allowCredentials: [
|
|
266
226
|
{
|
|
@@ -276,19 +236,11 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
276
236
|
}
|
|
277
237
|
);
|
|
278
238
|
|
|
279
|
-
// set sudo if this is a sudo request
|
|
280
|
-
if (keyId === 'sudo')
|
|
281
|
-
this.sudoKeyId = authenticationResponse.id
|
|
282
|
-
|
|
283
|
-
// reset this.keyId to be the most recently used passkey
|
|
284
|
-
this.keyId = authenticationResponse.id
|
|
285
|
-
|
|
286
239
|
const signatureRaw = base64url.toBuffer(authenticationResponse.response.signature);
|
|
287
240
|
const signature = this.convertEcdsaSignatureAsnToCompact(signatureRaw);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
creds.signature(xdr.ScVal.scvMap([
|
|
241
|
+
|
|
242
|
+
credentials.signatureExpirationLedger(lastLedger + ledgersToLive)
|
|
243
|
+
credentials.signature(xdr.ScVal.scvMap([
|
|
292
244
|
new xdr.ScMapEntry({
|
|
293
245
|
key: xdr.ScVal.scvSymbol('authenticator_data'),
|
|
294
246
|
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.authenticatorData)),
|
|
@@ -306,6 +258,69 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
306
258
|
val: xdr.ScVal.scvBytes(signature),
|
|
307
259
|
}),
|
|
308
260
|
]))
|
|
261
|
+
|
|
262
|
+
return entry
|
|
263
|
+
}
|
|
264
|
+
public async signAuthEntries(
|
|
265
|
+
entries: xdr.SorobanAuthorizationEntry[],
|
|
266
|
+
options?: {
|
|
267
|
+
keyId?: 'any' | 'sudo' | string | Uint8Array
|
|
268
|
+
ledgersToLive?: number
|
|
269
|
+
}
|
|
270
|
+
) {
|
|
271
|
+
for (const auth of entries) {
|
|
272
|
+
if (
|
|
273
|
+
auth.credentials().switch().name === 'sorobanCredentialsAddress'
|
|
274
|
+
&& auth.credentials().address().address().switch().name === 'scAddressTypeContract'
|
|
275
|
+
) {
|
|
276
|
+
// If auth entry matches our Smart Wallet move forward with the signature request
|
|
277
|
+
if (Address.contract(auth.credentials().address().address().contractId()).toString() === this.wallet!.options.contractId)
|
|
278
|
+
await this.signAuthEntry(auth, options)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return entries
|
|
283
|
+
}
|
|
284
|
+
/* NOTE
|
|
285
|
+
- Is there anything to be done with using the TS bindings `signAuthEntries` logic? Likely not but worth exploring
|
|
286
|
+
Perhaps not but maybe the js-sdk's `authorizeEntry` or `authorizeInvocation`
|
|
287
|
+
https://stellar.github.io/js-stellar-sdk/global.html#authorizeInvocation
|
|
288
|
+
These methods all only support ed25519 Keypairs
|
|
289
|
+
@No
|
|
290
|
+
*/
|
|
291
|
+
public async sign(
|
|
292
|
+
txn: Transaction | string,
|
|
293
|
+
options?: {
|
|
294
|
+
keyId?: 'any' | 'sudo' | string | Uint8Array
|
|
295
|
+
ledgersToLive?: number
|
|
296
|
+
}
|
|
297
|
+
) {
|
|
298
|
+
// Hack to ensure we don't stack fees when simulating and assembling multiple times
|
|
299
|
+
txn = TransactionBuilder.cloneFrom(new Transaction(
|
|
300
|
+
typeof txn === 'string'
|
|
301
|
+
? txn
|
|
302
|
+
: txn.toXDR(),
|
|
303
|
+
this.networkPassphrase
|
|
304
|
+
), { fee: '0' }).build()
|
|
305
|
+
|
|
306
|
+
if (txn.operations.length !== 1)
|
|
307
|
+
throw new Error('Must include only one Soroban operation')
|
|
308
|
+
|
|
309
|
+
for (const op of txn.operations) {
|
|
310
|
+
if (
|
|
311
|
+
op.type !== 'invokeHostFunction'
|
|
312
|
+
&& op.type !== 'extendFootprintTtl'
|
|
313
|
+
&& op.type !== 'restoreFootprint'
|
|
314
|
+
) throw new Error('Must include only one operation of type `invokeHostFunction` or `extendFootprintTtl` or `restoreFootprint`')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Only need to sign auth for `invokeHostFunction` operations
|
|
318
|
+
if (txn.operations[0].type === 'invokeHostFunction') {
|
|
319
|
+
const entries = (txn.operations[0] as Operation.InvokeHostFunction).auth
|
|
320
|
+
|
|
321
|
+
if (entries)
|
|
322
|
+
await this.signAuthEntries(entries, options)
|
|
323
|
+
}
|
|
309
324
|
|
|
310
325
|
const sim = await this.rpc.simulateTransaction(txn)
|
|
311
326
|
|
|
@@ -317,10 +332,10 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
317
332
|
return SorobanRpc.assembleTransaction(txn, sim).build().toXDR()
|
|
318
333
|
}
|
|
319
334
|
|
|
320
|
-
public async getData() {
|
|
335
|
+
public async getData(contractData?: SorobanRpc.Api.LedgerEntryResult) {
|
|
321
336
|
const data: Map<string, any> = new Map()
|
|
322
337
|
|
|
323
|
-
const { val } = await this.rpc.getContractData(
|
|
338
|
+
const { val } = contractData || await this.rpc.getContractData(
|
|
324
339
|
this.wallet!.options.contractId,
|
|
325
340
|
xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
326
341
|
);
|
package/types/kit.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Client as PasskeyClient } from 'passkey-kit-sdk';
|
|
2
2
|
import { Client as FactoryClient } from 'passkey-factory-sdk';
|
|
3
|
-
import { Networks, Transaction, SorobanRpc } from '@stellar/stellar-sdk';
|
|
3
|
+
import { Networks, xdr, Transaction, SorobanRpc } from '@stellar/stellar-sdk';
|
|
4
4
|
import { Buffer } from 'buffer';
|
|
5
5
|
import { PasskeyBase } from './base';
|
|
6
6
|
export declare class PasskeyKit extends PasskeyBase {
|
|
@@ -13,11 +13,11 @@ export declare class PasskeyKit extends PasskeyBase {
|
|
|
13
13
|
rpc: SorobanRpc.Server;
|
|
14
14
|
factoryContractId: string;
|
|
15
15
|
constructor(options: {
|
|
16
|
-
networkPassphrase: Networks;
|
|
17
16
|
rpcUrl: string;
|
|
18
17
|
launchtubeUrl?: string;
|
|
19
18
|
launchtubeJwt?: string;
|
|
20
19
|
factoryContractId?: string;
|
|
20
|
+
networkPassphrase: Networks;
|
|
21
21
|
});
|
|
22
22
|
createWallet(name: string, user: string): Promise<{
|
|
23
23
|
keyId: Buffer;
|
|
@@ -28,15 +28,23 @@ export declare class PasskeyKit extends PasskeyBase {
|
|
|
28
28
|
keyId: Buffer;
|
|
29
29
|
publicKey: Buffer;
|
|
30
30
|
}>;
|
|
31
|
-
connectWallet(
|
|
31
|
+
connectWallet(keyId?: string): Promise<{
|
|
32
32
|
keyId: Buffer;
|
|
33
33
|
contractId: string;
|
|
34
34
|
}>;
|
|
35
|
+
signAuthEntry(entry: xdr.SorobanAuthorizationEntry, options?: {
|
|
36
|
+
keyId?: 'any' | 'sudo' | string | Uint8Array;
|
|
37
|
+
ledgersToLive?: number;
|
|
38
|
+
}): Promise<xdr.SorobanAuthorizationEntry>;
|
|
39
|
+
signAuthEntries(entries: xdr.SorobanAuthorizationEntry[], options?: {
|
|
40
|
+
keyId?: 'any' | 'sudo' | string | Uint8Array;
|
|
41
|
+
ledgersToLive?: number;
|
|
42
|
+
}): Promise<xdr.SorobanAuthorizationEntry[]>;
|
|
35
43
|
sign(txn: Transaction | string, options?: {
|
|
36
44
|
keyId?: 'any' | 'sudo' | string | Uint8Array;
|
|
37
45
|
ledgersToLive?: number;
|
|
38
46
|
}): Promise<string>;
|
|
39
|
-
getData(): Promise<Map<string, any>>;
|
|
47
|
+
getData(contractData?: SorobanRpc.Api.LedgerEntryResult): Promise<Map<string, any>>;
|
|
40
48
|
private getPublicKeyObject;
|
|
41
49
|
private convertEcdsaSignatureAsnToCompact;
|
|
42
50
|
}
|