passkey-kit 0.2.1 → 0.2.4

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.
Files changed (3) hide show
  1. package/package.json +3 -3
  2. package/src/kit.ts +103 -88
  3. package/types/kit.d.ts +12 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passkey-kit",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "description": "A helper library for creating and using passkey accounts on the Stellar blockchain.",
5
5
  "author": "Tyler van der Hoeven",
6
6
  "license": "MIT",
@@ -14,8 +14,8 @@
14
14
  "bigint-conversion": "^2.4.3",
15
15
  "buffer": "^6.0.3",
16
16
  "cbor-x": "^1.5.9",
17
- "passkey-factory-sdk": "0.0.8",
18
- "passkey-kit-sdk": "0.0.8"
17
+ "passkey-kit-sdk": "0.0.8",
18
+ "passkey-factory-sdk": "0.0.8"
19
19
  },
20
20
  "devDependencies": {
21
21
  "typescript": "^5.4.5"
package/src/kit.ts CHANGED
@@ -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
- /* TODO
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
- @Later
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(id?: string) {
143
- /* TODO
144
- - Support passing in a contractId as well as a keyId
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 = startAuthenticationResponse.id
147
+ this.keyId = keyId
168
148
 
169
- const keyIdBuffer = base64url.toBuffer(startAuthenticationResponse.id)
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,9 +161,11 @@ 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 {
@@ -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
- /* TODO
210
- - Is there anything to be done with using the TS bindings `signAuthEntries` logic? Likely not but worth exploring
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 authHash = hash(
243
- xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
244
- new xdr.HashIdPreimageSorobanAuthorization({
245
- networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
246
- nonce: auth.credentials().address().nonce(),
247
- signatureExpirationLedger: lastLedger + ledgersToLive,
248
- invocation: auth.rootInvocation()
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(authHash),
218
+ challenge: base64url(payload),
259
219
  // rpId: undefined,
260
220
  userVerification: "discouraged",
261
221
  }
262
222
  : {
263
- challenge: base64url(authHash),
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
- const creds = auth.credentials().address();
289
-
290
- creds.signatureExpirationLedger(lastLedger + ledgersToLive)
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(id?: string): Promise<{
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
  }