passkey-kit 0.1.1 → 0.2.1

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/cheatsheet ADDED
@@ -0,0 +1,5 @@
1
+ # Install contract sdks
2
+ npm publish --workspaces
3
+
4
+ # Install passkey-kit
5
+ pnpm publish --no-git-checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passkey-kit",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
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",
@@ -9,14 +9,13 @@
9
9
  "types": "types/index.d.ts",
10
10
  "dependencies": {
11
11
  "@simplewebauthn/browser": "^10.0.0",
12
- "@simplewebauthn/types": "^10.0.0",
13
12
  "@stellar/stellar-sdk": "12.0.1",
14
13
  "base64url": "^3.0.1",
15
14
  "bigint-conversion": "^2.4.3",
16
15
  "buffer": "^6.0.3",
17
16
  "cbor-x": "^1.5.9",
18
- "passkey-factory-sdk": "0.0.7",
19
- "passkey-kit-sdk": "0.0.7"
17
+ "passkey-factory-sdk": "0.0.8",
18
+ "passkey-kit-sdk": "0.0.8"
20
19
  },
21
20
  "devDependencies": {
22
21
  "typescript": "^5.4.5"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.7",
2
+ "version": "0.0.8",
3
3
  "name": "passkey-factory-sdk",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
@@ -1,12 +1,10 @@
1
1
  import { Buffer } from "buffer";
2
2
  import {
3
- Client as ContractClient,
4
- Spec as ContractSpec,
5
- } from '@stellar/stellar-sdk/contract';
6
- import type {
7
3
  AssembledTransaction,
4
+ Client as ContractClient,
8
5
  ClientOptions as ContractClientOptions,
9
6
  Result,
7
+ Spec as ContractSpec,
10
8
  } from '@stellar/stellar-sdk/contract';
11
9
 
12
10
  if (typeof window !== 'undefined') {
@@ -17,7 +15,7 @@ if (typeof window !== 'undefined') {
17
15
  export const networks = {
18
16
  testnet: {
19
17
  networkPassphrase: "Test SDF Network ; September 2015",
20
- contractId: "CDSGG7BSWYWQMTY5KTZVDTC34ZESMZ75ZGSTTVHS4NIKCF4PM3GDJ4G3",
18
+ contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ",
21
19
  }
22
20
  } as const
23
21
 
@@ -1,10 +1,9 @@
1
1
  import { Buffer } from "buffer";
2
- import { Client as ContractClient } from '@stellar/stellar-sdk/contract';
3
- import type { AssembledTransaction, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
2
+ import { AssembledTransaction, Client as ContractClient, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
4
3
  export declare const networks: {
5
4
  readonly testnet: {
6
5
  readonly networkPassphrase: "Test SDF Network ; September 2015";
7
- readonly contractId: "CDSGG7BSWYWQMTY5KTZVDTC34ZESMZ75ZGSTTVHS4NIKCF4PM3GDJ4G3";
6
+ readonly contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ";
8
7
  };
9
8
  };
10
9
  export declare const Errors: {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.7",
2
+ "version": "0.0.8",
3
3
  "name": "passkey-kit-sdk",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
@@ -1,12 +1,10 @@
1
1
  import { Buffer } from "buffer";
2
2
  import {
3
- Client as ContractClient,
4
- Spec as ContractSpec,
5
- } from '@stellar/stellar-sdk/contract';
6
- import type {
7
3
  AssembledTransaction,
4
+ Client as ContractClient,
8
5
  ClientOptions as ContractClientOptions,
9
6
  Result,
7
+ Spec as ContractSpec,
10
8
  } from '@stellar/stellar-sdk/contract';
11
9
 
12
10
  if (typeof window !== 'undefined') {
@@ -17,7 +15,7 @@ if (typeof window !== 'undefined') {
17
15
  export const networks = {
18
16
  testnet: {
19
17
  networkPassphrase: "Test SDF Network ; September 2015",
20
- contractId: "CDSGG7BSWYWQMTY5KTZVDTC34ZESMZ75ZGSTTVHS4NIKCF4PM3GDJ4G3",
18
+ contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ",
21
19
  }
22
20
  } as const
23
21
 
@@ -82,6 +80,26 @@ export interface Client {
82
80
  simulate?: boolean;
83
81
  }) => Promise<AssembledTransaction<Result<void>>>
84
82
 
83
+ /**
84
+ * Construct and simulate a upgrade transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
85
+ */
86
+ upgrade: ({ hash }: { hash: Buffer }, options?: {
87
+ /**
88
+ * The fee to pay for the transaction. Default: BASE_FEE
89
+ */
90
+ fee?: number;
91
+
92
+ /**
93
+ * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT
94
+ */
95
+ timeoutInSeconds?: number;
96
+
97
+ /**
98
+ * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true
99
+ */
100
+ simulate?: boolean;
101
+ }) => Promise<AssembledTransaction<Result<void>>>
102
+
85
103
  /**
86
104
  * Construct and simulate a resudo transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
87
105
  */
@@ -148,6 +166,7 @@ export class Client extends ContractClient {
148
166
  new ContractSpec(["AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAACgAAAAAAAAAJTm90SW5pdGVkAAAAAAAAAQAAAAAAAAAITm90Rm91bmQAAAACAAAAAAAAAAxOb3RQZXJtaXR0ZWQAAAADAAAAAAAAAA1BbHJlYWR5SW5pdGVkAAAAAAAABAAAAAAAAAAOSnNvblBhcnNlRXJyb3IAAAAAAAUAAAAAAAAADkludmFsaWRDb250ZXh0AAAAAAAGAAAAAAAAACBDbGllbnREYXRhSnNvbkNoYWxsZW5nZUluY29ycmVjdAAAAAcAAAAAAAAAF1NlY3AyNTZyMVB1YmxpY0tleVBhcnNlAAAAAAgAAAAAAAAAF1NlY3AyNTZyMVNpZ25hdHVyZVBhcnNlAAAAAAkAAAAAAAAAFVNlY3AyNTZyMVZlcmlmeUZhaWxlZAAAAAAAAAo=",
149
167
  "AAAAAAAAAAAAAAAKZXh0ZW5kX3R0bAAAAAAAAAAAAAA=",
150
168
  "AAAAAAAAAAAAAAAEaW5pdAAAAAMAAAAAAAAAAmlkAAAAAAAOAAAAAAAAAAJwawAAAAAD7gAAAEEAAAAAAAAAB2ZhY3RvcnkAAAAAEwAAAAEAAAPpAAAD7QAAAAAAAAAD",
169
+ "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAARoYXNoAAAD7gAAACAAAAABAAAD6QAAA+0AAAAAAAAAAw==",
151
170
  "AAAAAAAAAAAAAAAGcmVzdWRvAAAAAAABAAAAAAAAAAJpZAAAAAAADgAAAAEAAAPpAAAD7QAAAAAAAAAD",
152
171
  "AAAAAAAAAAAAAAAGcm1fc2lnAAAAAAABAAAAAAAAAAJpZAAAAAAADgAAAAEAAAPpAAAD7QAAAAAAAAAD",
153
172
  "AAAAAAAAAAAAAAAHYWRkX3NpZwAAAAACAAAAAAAAAAJpZAAAAAAADgAAAAAAAAACcGsAAAAAA+4AAABBAAAAAQAAA+kAAAPtAAAAAAAAAAM=",
@@ -159,6 +178,7 @@ export class Client extends ContractClient {
159
178
  public readonly fromJSON = {
160
179
  extend_ttl: this.txFromJSON<null>,
161
180
  init: this.txFromJSON<Result<void>>,
181
+ upgrade: this.txFromJSON<Result<void>>,
162
182
  resudo: this.txFromJSON<Result<void>>,
163
183
  rm_sig: this.txFromJSON<Result<void>>,
164
184
  add_sig: this.txFromJSON<Result<void>>,
@@ -1,10 +1,9 @@
1
1
  import { Buffer } from "buffer";
2
- import { Client as ContractClient } from '@stellar/stellar-sdk/contract';
3
- import type { AssembledTransaction, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
2
+ import { AssembledTransaction, Client as ContractClient, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
4
3
  export declare const networks: {
5
4
  readonly testnet: {
6
5
  readonly networkPassphrase: "Test SDF Network ; September 2015";
7
- readonly contractId: "CDSGG7BSWYWQMTY5KTZVDTC34ZESMZ75ZGSTTVHS4NIKCF4PM3GDJ4G3";
6
+ readonly contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ";
8
7
  };
9
8
  };
10
9
  export declare const Errors: {
@@ -84,6 +83,25 @@ export interface Client {
84
83
  */
85
84
  simulate?: boolean;
86
85
  }) => Promise<AssembledTransaction<Result<void>>>;
86
+ /**
87
+ * Construct and simulate a upgrade transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
88
+ */
89
+ upgrade: ({ hash }: {
90
+ hash: Buffer;
91
+ }, options?: {
92
+ /**
93
+ * The fee to pay for the transaction. Default: BASE_FEE
94
+ */
95
+ fee?: number;
96
+ /**
97
+ * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT
98
+ */
99
+ timeoutInSeconds?: number;
100
+ /**
101
+ * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true
102
+ */
103
+ simulate?: boolean;
104
+ }) => Promise<AssembledTransaction<Result<void>>>;
87
105
  /**
88
106
  * Construct and simulate a resudo transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
89
107
  */
@@ -149,6 +167,7 @@ export declare class Client extends ContractClient {
149
167
  readonly fromJSON: {
150
168
  extend_ttl: (json: string) => AssembledTransaction<null>;
151
169
  init: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
170
+ upgrade: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
152
171
  resudo: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
153
172
  rm_sig: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
154
173
  add_sig: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
package/src/base.ts ADDED
@@ -0,0 +1,42 @@
1
+ export class PasskeyBase {
2
+ public launchtubeUrl: string | undefined
3
+ public launchtubeJwt: string | undefined
4
+
5
+ constructor(options: {
6
+ launchtubeUrl?: string,
7
+ launchtubeJwt?: string,
8
+ }) {
9
+ const {
10
+ launchtubeUrl,
11
+ launchtubeJwt,
12
+ } = options
13
+
14
+ if (launchtubeUrl)
15
+ this.launchtubeUrl = launchtubeUrl
16
+
17
+ if (launchtubeJwt)
18
+ this.launchtubeJwt = launchtubeJwt
19
+ }
20
+
21
+ public async send(xdr: string, fee: number = 10_000) {
22
+ if (!this.launchtubeUrl || !this.launchtubeJwt)
23
+ throw new Error('Launchtube service not configured')
24
+
25
+ const data = new FormData();
26
+
27
+ data.set('xdr', xdr);
28
+ data.set('fee', fee.toString());
29
+
30
+ return fetch(this.launchtubeUrl, {
31
+ method: 'POST',
32
+ headers: {
33
+ authorization: `Bearer ${this.launchtubeJwt}`,
34
+ },
35
+ body: data
36
+ }).then(async (res) => {
37
+ if (res.ok)
38
+ return res.json()
39
+ else throw await res.json()
40
+ })
41
+ }
42
+ }
package/src/index.ts CHANGED
@@ -1,507 +1,4 @@
1
- import { Client as PasskeyClient } from 'passkey-kit-sdk'
2
- import { Client as FactoryClient, networks } from 'passkey-factory-sdk'
3
- import { Address, Networks, StrKey, hash, xdr, Transaction, Horizon, FeeBumpTransaction, SorobanRpc, Operation, scValToNative, TransactionBuilder } from '@stellar/stellar-sdk'
4
- import { bufToBigint, bigintToBuf } from 'bigint-conversion'
5
- import base64url from 'base64url'
6
- import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
7
- import { decode } from 'cbor-x/decode'
8
- import type { RegistrationResponseJSON } from '@simplewebauthn/types';
9
- import { Buffer } from 'buffer'
1
+ import { PasskeyBase } from "./base"
2
+ import { PasskeyKit } from "./kit"
10
3
 
11
- /* TODO
12
- - Clean up these params and the interface as a whole
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
- @Later
15
-
16
- - Right now publicKey can mean a Stellar public key or a passkey public key, there should be a noted difference
17
- */
18
-
19
- export class PasskeyAccount {
20
- public keyId: string | undefined
21
- public sudoKeyId: string | undefined
22
- public wallet: PasskeyClient | undefined
23
- public factory: FactoryClient
24
- public sequencePublicKey: string
25
- public networkPassphrase: Networks
26
- public horizonUrl: string
27
- public horizon: Horizon.Server
28
- public rpcUrl: string
29
- public rpc: SorobanRpc.Server
30
- public feeBumpUrl: string | undefined
31
- public feeBumpJwt: string | undefined
32
- public factoryContractId: string = networks.testnet.contractId
33
-
34
- /* TODO
35
- - Consider adding the ability to pass in a keyId and maybe even a contractId in order to preconnect to a wallet
36
- If just a keyId call `connectWallet` in order to get the contractId
37
- 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)
38
- We don't stictly _need_ this as a dev can just call `connectWallet` after class instantiation but it might be a nice convenience
39
- @Later
40
- */
41
- constructor(options: {
42
- sequencePublicKey: string,
43
- networkPassphrase: Networks,
44
- horizonUrl: string,
45
- rpcUrl: string,
46
- feeBumpUrl?: string,
47
- feeBumpJwt?: string,
48
- /* TODO
49
- - Maybe remove this? The factory should likely be baked in a bit more tightly
50
- On the other hand once we have a standard interface the factory interface only uses the `deploy` method right now inside the interface
51
- @Later
52
- */
53
- factoryContractId?: string,
54
- }) {
55
- const {
56
- sequencePublicKey,
57
- networkPassphrase,
58
- horizonUrl,
59
- rpcUrl,
60
- feeBumpUrl,
61
- feeBumpJwt,
62
- factoryContractId
63
- } = options
64
-
65
- this.sequencePublicKey = sequencePublicKey
66
- this.networkPassphrase = networkPassphrase
67
- this.horizonUrl = horizonUrl
68
- this.horizon = new Horizon.Server(horizonUrl)
69
- this.rpcUrl = rpcUrl
70
- this.rpc = new SorobanRpc.Server(rpcUrl)
71
-
72
- if (feeBumpUrl)
73
- this.feeBumpUrl = feeBumpUrl
74
-
75
- if (feeBumpJwt)
76
- this.feeBumpJwt = feeBumpJwt
77
-
78
- if (factoryContractId)
79
- this.factoryContractId = factoryContractId
80
-
81
- this.factory = new FactoryClient({
82
- publicKey: sequencePublicKey,
83
- contractId: this.factoryContractId,
84
- networkPassphrase,
85
- rpcUrl
86
- })
87
- }
88
-
89
- public async createWallet(name: string, user: string) {
90
- const { keyId, publicKey } = await this.createKey(name, user)
91
-
92
- const { result, built } = await this.factory.deploy({
93
- id: keyId,
94
- pk: publicKey!
95
- })
96
-
97
- const contractId = result.unwrap() as string
98
-
99
- this.wallet = new PasskeyClient({
100
- publicKey: this.sequencePublicKey,
101
- contractId,
102
- networkPassphrase: this.networkPassphrase,
103
- rpcUrl: this.rpcUrl
104
- })
105
-
106
- return {
107
- keyId,
108
- contractId,
109
- xdr: built!.toXDR() as string
110
- }
111
- }
112
-
113
- public async createKey(name: string, user: string) {
114
- const startRegistrationResponse = await startRegistration({
115
- challenge: base64url("stellaristhebetterblockchain"),
116
- rp: {
117
- // id: undefined,
118
- name,
119
- },
120
- user: {
121
- id: base64url(user),
122
- name: user,
123
- displayName: user,
124
- },
125
- authenticatorSelection: {
126
- requireResidentKey: false,
127
- residentKey: "preferred",
128
- userVerification: "discouraged",
129
- },
130
- pubKeyCredParams: [{ alg: -7, type: "public-key" }],
131
- attestation: "none",
132
- });
133
-
134
- if (!this.keyId) {
135
- this.keyId = startRegistrationResponse.id
136
-
137
- // If there was no keyId we're likely about to deploy a new wallet so we should set the sudoKeyId
138
- if (!this.sudoKeyId)
139
- this.sudoKeyId = startRegistrationResponse.id
140
- }
141
-
142
- const { publicKeyObject } = this.getPublicKeyObject(startRegistrationResponse.response.attestationObject);
143
-
144
- const publicKey = Buffer.from([
145
- 4, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
146
- ...publicKeyObject.get('-2')!,
147
- ...publicKeyObject.get('-3')!
148
- ])
149
-
150
- return {
151
- keyId: base64url.toBuffer(startRegistrationResponse.id),
152
- publicKey
153
- }
154
- }
155
-
156
- public async connectWallet(id?: string) {
157
- /* TODO
158
- - Support passing in a contractId as well as a keyId
159
- Maybe not as we wouldn't have a keyId which could have interesting consequences
160
- Also not sure what the use case would be for this where keyId wouldn't also be possible and better
161
- @No
162
- */
163
-
164
- // @ts-ignore
165
- // https://github.com/stellar/js-stellar-base/issues/750
166
- // if (id && StrKey.isValidContract(id)) {
167
-
168
- // } else {
169
-
170
- // }
171
-
172
- const startAuthenticationResponse = id
173
- ? { id }
174
- : await startAuthentication({
175
- challenge: base64url("stellaristhebetterblockchain"),
176
- // rpId: undefined,
177
- userVerification: "discouraged",
178
- });
179
-
180
- if (!this.keyId)
181
- this.keyId = startAuthenticationResponse.id
182
-
183
- const keyIdBuffer = base64url.toBuffer(startAuthenticationResponse.id)
184
-
185
- // 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
186
- let contractId = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
187
- new xdr.HashIdPreimageContractId({
188
- networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
189
- contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
190
- new xdr.ContractIdPreimageFromAddress({
191
- address: Address.fromString(this.factoryContractId).toScAddress(),
192
- salt: hash(keyIdBuffer),
193
- })
194
- )
195
- })
196
- ).toXDR()));
197
-
198
- // attempt passkey id derivation
199
- try {
200
- await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
201
- }
202
- // if that fails look up from the factory mapper
203
- catch {
204
- const { val } = await this.rpc.getContractData(this.factoryContractId, xdr.ScVal.scvBytes(keyIdBuffer))
205
- contractId = scValToNative(val.contractData().val())
206
- }
207
-
208
- this.wallet = new PasskeyClient({
209
- publicKey: this.sequencePublicKey,
210
- contractId,
211
- networkPassphrase: this.networkPassphrase,
212
- rpcUrl: this.rpcUrl
213
- })
214
-
215
- // get and set the sudo signer
216
- await this.getData()
217
-
218
- return {
219
- keyId: keyIdBuffer,
220
- contractId
221
- }
222
- }
223
-
224
- public async sign(
225
- txn: Transaction | string,
226
- options?: {
227
- keyId?: 'any' | 'sudo' | string | Uint8Array
228
- ledgersToLive?: number
229
- }
230
- ) {
231
- // Default mirrors DEFAULT_TIMEOUT (currently 5 minutes) https://github.com/stellar/js-stellar-sdk/blob/master/src/contract/utils.ts#L7
232
- let { keyId, ledgersToLive = 60 } = options || {}
233
-
234
- // hack to ensure we don't stack fees when simulating and assembling multiple times
235
- txn = TransactionBuilder.cloneFrom(new Transaction(
236
- typeof txn === 'string'
237
- ? txn
238
- : txn.toXDR(),
239
- this.networkPassphrase
240
- ), { fee: '0' }).build()
241
-
242
- // NOTE hard coded to sign only Soroban transactions and only and always the first auth
243
- // TODO switch to support signing multiple and specific auth entries. Possibly pass in auth entry vs the entire txn
244
- const op = txn.operations[0] as Operation.InvokeHostFunction
245
- const auth = op.auth![0]
246
- const lastLedger = await this.rpc.getLatestLedger().then(({ sequence }) => sequence)
247
- const authHash = hash(
248
- xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
249
- new xdr.HashIdPreimageSorobanAuthorization({
250
- networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
251
- nonce: auth.credentials().address().nonce(),
252
- signatureExpirationLedger: lastLedger + ledgersToLive,
253
- invocation: auth.rootInvocation()
254
- })
255
- ).toXDR()
256
- )
257
-
258
- const authenticationResponse = await startAuthentication(
259
- keyId === 'any' || (keyId === 'sudo' && !this.sudoKeyId) || (!keyId && !this.keyId)
260
- ? {
261
- challenge: base64url(authHash),
262
- // rpId: undefined,
263
- userVerification: "discouraged",
264
- }
265
- : {
266
- challenge: base64url(authHash),
267
- // rpId: undefined,
268
- allowCredentials: [
269
- {
270
- id: keyId === 'sudo'
271
- ? this.sudoKeyId!
272
- : keyId instanceof Uint8Array
273
- ? base64url(keyId)
274
- : keyId || this.keyId!,
275
- type: "public-key",
276
- },
277
- ],
278
- userVerification: "discouraged",
279
- }
280
- );
281
-
282
- // set sudo if this is a sudo request
283
- if (keyId === 'sudo')
284
- this.sudoKeyId = authenticationResponse.id
285
-
286
- // reset this.keyId to be the most recently used passkey
287
- this.keyId = authenticationResponse.id
288
-
289
- const signatureRaw = base64url.toBuffer(authenticationResponse.response.signature);
290
- const signature = this.convertEcdsaSignatureAsnToCompact(signatureRaw);
291
- const creds = auth.credentials().address();
292
-
293
- creds.signatureExpirationLedger(lastLedger + ledgersToLive)
294
- creds.signature(xdr.ScVal.scvMap([
295
- new xdr.ScMapEntry({
296
- key: xdr.ScVal.scvSymbol('authenticator_data'),
297
- val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.authenticatorData)),
298
- }),
299
- new xdr.ScMapEntry({
300
- key: xdr.ScVal.scvSymbol('client_data_json'),
301
- val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.clientDataJSON)),
302
- }),
303
- new xdr.ScMapEntry({
304
- key: xdr.ScVal.scvSymbol('id'),
305
- val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.id)),
306
- }),
307
- new xdr.ScMapEntry({
308
- key: xdr.ScVal.scvSymbol('signature'),
309
- val: xdr.ScVal.scvBytes(signature),
310
- }),
311
- ]))
312
-
313
- const sim = await this.rpc.simulateTransaction(txn)
314
-
315
- if (
316
- SorobanRpc.Api.isSimulationError(sim)
317
- || SorobanRpc.Api.isSimulationRestore(sim) // TODO handle restore flow
318
- ) throw sim
319
-
320
- return SorobanRpc.assembleTransaction(txn, sim).build().toXDR()
321
- }
322
-
323
- public async send(txn: Transaction, fee: number = 10_000) {
324
- const data = new FormData();
325
-
326
- data.set('xdr', txn.toXDR());
327
- data.set('fee', fee.toString());
328
-
329
- const bumptxn = await fetch(this.feeBumpUrl!, {
330
- method: 'POST',
331
- headers: {
332
- authorization: `Bearer ${this.feeBumpJwt}`,
333
- },
334
- body: data
335
- }).then(async (res) => {
336
- if (res.ok)
337
- return res.text()
338
- else throw await res.json()
339
- })
340
-
341
- return this.horizon.submitTransaction(new FeeBumpTransaction(bumptxn, this.networkPassphrase))
342
- }
343
-
344
- public async getData() {
345
- const data: Map<string, any> = new Map()
346
-
347
- const { val } = await this.rpc.getContractData(
348
- this.wallet!.options.contractId,
349
- xdr.ScVal.scvLedgerKeyContractInstance(),
350
- );
351
-
352
- val.contractData()
353
- .val()
354
- .instance()
355
- .storage()
356
- ?.forEach((entry) => {
357
- data.set(
358
- scValToNative(entry.key()),
359
- scValToNative(entry.val()),
360
- );
361
- });
362
-
363
- this.sudoKeyId = base64url(data.get('sudo_sig'))
364
-
365
- return data
366
- }
367
-
368
- /* TODO
369
- - Add a getKeyInfo action to get info about a specific passkey
370
- Specifically looking for name, type, etc. data so a user could grok what signer mapped to what passkey
371
- @Later
372
- */
373
-
374
- private getPublicKeyObject(attestationObject: string) {
375
- const { authData } = decode(base64url.toBuffer(attestationObject));
376
- const authDataUint8Array = new Uint8Array(authData);
377
- const authDataView = new DataView(authDataUint8Array.buffer, 0, authDataUint8Array.length);
378
-
379
- let offset = 0;
380
-
381
- // RP ID Hash (32 bytes)
382
- const rpIdHash = authData.slice(offset, offset + 32);
383
- offset += 32;
384
-
385
- // Flags (1 byte)
386
- const flags = authDataView.getUint8(offset);
387
- offset += 1;
388
-
389
- // Sign Count (4 bytes, big endian)
390
- const signCount = authDataView.getUint32(offset, false);
391
- offset += 4;
392
-
393
- // Attested Credential Data, if present
394
- if (flags & 0x40) { // Checking the AT flag
395
- // AAGUID (16 bytes)
396
- const aaguid = authData.slice(offset, offset + 16);
397
- offset += 16;
398
-
399
- // Credential ID Length (2 bytes, big endian)
400
- const credIdLength = authDataView.getUint16(offset, false);
401
- offset += 2;
402
-
403
- // Credential ID (variable length)
404
- const credentialId = authData.slice(offset, offset + credIdLength);
405
- offset += credIdLength;
406
-
407
- // Credential Public Key - (77 bytes...I hope)
408
- const credentialPublicKey = authData.slice(offset, offset + 77);
409
- offset += 77;
410
-
411
- // Any leftover bytes. I found some when using a YubiKey
412
- const theRest = authData.slice(offset);
413
-
414
- // Decode the credential public key to COSE
415
- const publicKeyObject = new Map<string, any>(Object.entries(decode(credentialPublicKey)));
416
-
417
- return {
418
- rpIdHash,
419
- flags,
420
- signCount,
421
- aaguid,
422
- credIdLength,
423
- credentialId,
424
- credentialPublicKey,
425
- theRest,
426
- publicKeyObject
427
- };
428
- }
429
-
430
- throw new Error("Attested credential data not present in the flags.");
431
- }
432
-
433
- private convertEcdsaSignatureAsnToCompact(sig: Buffer) {
434
- // Define the order of the curve secp256k1
435
- // https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
436
- const q = Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex')
437
-
438
- // ASN Sequence
439
- let offset = 0;
440
- if (sig[offset] != 0x30) {
441
- throw "signature is not a sequence";
442
- }
443
- offset += 1;
444
-
445
- // ASN Sequence Byte Length
446
- offset += 1;
447
-
448
- // ASN Integer (R)
449
- if (sig[offset] != 0x02) {
450
- throw "first element in sequence is not an integer";
451
- }
452
- offset += 1;
453
-
454
- // ASN Integer (R) Byte Length
455
- const rLen = sig[offset];
456
- offset += 1;
457
-
458
- // ASN Integer (R) Byte Value
459
- if (rLen >= 33) {
460
- if (rLen != 33 || sig[offset] != 0x00) {
461
- throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
462
- }
463
- offset += 1;
464
- }
465
- const r = sig.slice(offset, offset + 32);
466
- offset += 32;
467
-
468
- // ASN Integer (S)
469
- if (sig[offset] != 0x02) {
470
- throw "second element in sequence is not an integer";
471
- }
472
- offset += 1;
473
-
474
- // ASN Integer (S) Byte Length
475
- const sLen = sig[offset];
476
- offset += 1;
477
-
478
- // ASN Integer (S) Byte Value
479
- if (sLen >= 33) {
480
- if (sLen != 33 || sig[offset] != 0x00) {
481
- throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
482
- }
483
- offset += 1;
484
- }
485
-
486
- const s = sig.slice(offset, offset + 32);
487
-
488
- offset += 32;
489
-
490
- let signature64: Buffer
491
-
492
- // Force low S range
493
- // https://github.com/stellar/stellar-protocol/discussions/1435#discussioncomment-8809175
494
- // https://discord.com/channels/897514728459468821/1233048618571927693
495
- if (bufToBigint(s) > ((bufToBigint(q) - BigInt(1)) / BigInt(2))) {
496
- signature64 = Buffer.from([...r, ...Buffer.from(bigintToBuf(bufToBigint(q) - bufToBigint(s), true) as ArrayBuffer)]);
497
- } else {
498
- signature64 = Buffer.from([...r, ...s]);
499
- }
500
-
501
- return signature64;
502
- }
503
- }
504
-
505
- function isAuthentication(value: any): value is RegistrationResponseJSON {
506
- return value?.response?.attestationObject;
507
- }
4
+ export { PasskeyBase, PasskeyKit }
package/src/kit.ts ADDED
@@ -0,0 +1,479 @@
1
+ import { Client as PasskeyClient } from 'passkey-kit-sdk'
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'
4
+ import { bufToBigint, bigintToBuf } from 'bigint-conversion'
5
+ import base64url from 'base64url'
6
+ import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
7
+ import { decode } from 'cbor-x/decode'
8
+ import { Buffer } from 'buffer'
9
+ import { PasskeyBase } from './base'
10
+
11
+ /* TODO
12
+ - Clean up these params and the interface as a whole
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
+ @Later
15
+
16
+ - Right now publicKey can mean a Stellar public key or a passkey public key, there should be a noted difference
17
+ */
18
+
19
+ export class PasskeyKit extends PasskeyBase {
20
+ public keyId: string | undefined
21
+ public sudoKeyId: string | undefined
22
+ public wallet: PasskeyClient | undefined
23
+ public factory: FactoryClient
24
+ public networkPassphrase: Networks
25
+ public rpcUrl: string
26
+ public rpc: SorobanRpc.Server
27
+ public factoryContractId: string = networks.testnet.contractId
28
+
29
+ /* TODO
30
+ - Consider adding the ability to pass in a keyId and maybe even a contractId in order to preconnect to a wallet
31
+ If just a keyId call `connectWallet` in order to get the contractId
32
+ 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
+ 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
35
+ */
36
+
37
+ constructor(options: {
38
+ networkPassphrase: Networks,
39
+ rpcUrl: string,
40
+ launchtubeUrl?: string,
41
+ 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
+ factoryContractId?: string,
48
+ }) {
49
+ const {
50
+ networkPassphrase,
51
+ rpcUrl,
52
+ launchtubeUrl,
53
+ launchtubeJwt,
54
+ factoryContractId
55
+ } = options
56
+
57
+ super({
58
+ launchtubeUrl,
59
+ launchtubeJwt,
60
+ })
61
+
62
+ this.networkPassphrase = networkPassphrase
63
+ this.rpcUrl = rpcUrl
64
+ this.rpc = new SorobanRpc.Server(rpcUrl)
65
+
66
+ if (factoryContractId)
67
+ this.factoryContractId = factoryContractId
68
+
69
+ this.factory = new FactoryClient({
70
+ contractId: this.factoryContractId,
71
+ networkPassphrase,
72
+ rpcUrl
73
+ })
74
+ }
75
+
76
+ public async createWallet(name: string, user: string) {
77
+ const { keyId, publicKey } = await this.createKey(name, user)
78
+
79
+ const { result, built } = await this.factory.deploy({
80
+ id: keyId,
81
+ pk: publicKey!
82
+ })
83
+
84
+ const contractId = result.unwrap() as string
85
+
86
+ this.wallet = new PasskeyClient({
87
+ contractId,
88
+ networkPassphrase: this.networkPassphrase,
89
+ rpcUrl: this.rpcUrl
90
+ })
91
+
92
+ return {
93
+ keyId,
94
+ contractId,
95
+ xdr: built!.toXDR() as string
96
+ }
97
+ }
98
+
99
+ public async createKey(name: string, user: string) {
100
+ const startRegistrationResponse = await startRegistration({
101
+ challenge: base64url("stellaristhebetterblockchain"),
102
+ rp: {
103
+ // id: undefined,
104
+ name,
105
+ },
106
+ user: {
107
+ id: base64url(user),
108
+ name: user,
109
+ displayName: user,
110
+ },
111
+ authenticatorSelection: {
112
+ requireResidentKey: false,
113
+ residentKey: "preferred",
114
+ userVerification: "discouraged",
115
+ },
116
+ pubKeyCredParams: [{ alg: -7, type: "public-key" }],
117
+ attestation: "none",
118
+ });
119
+
120
+ if (!this.keyId) {
121
+ this.keyId = startRegistrationResponse.id
122
+
123
+ // If there was no keyId we're likely about to deploy a new wallet so we should set the sudoKeyId
124
+ if (!this.sudoKeyId)
125
+ this.sudoKeyId = startRegistrationResponse.id
126
+ }
127
+
128
+ const { publicKeyObject } = this.getPublicKeyObject(startRegistrationResponse.response.attestationObject);
129
+
130
+ const publicKey = Buffer.from([
131
+ 4, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
132
+ ...publicKeyObject.get('-2')!,
133
+ ...publicKeyObject.get('-3')!
134
+ ])
135
+
136
+ return {
137
+ keyId: base64url.toBuffer(startRegistrationResponse.id),
138
+ publicKey
139
+ }
140
+ }
141
+
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({
161
+ challenge: base64url("stellaristhebetterblockchain"),
162
+ // rpId: undefined,
163
+ userVerification: "discouraged",
164
+ });
165
+
166
+ if (!this.keyId)
167
+ this.keyId = startAuthenticationResponse.id
168
+
169
+ const keyIdBuffer = base64url.toBuffer(startAuthenticationResponse.id)
170
+
171
+ // 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
+ let contractId = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
173
+ new xdr.HashIdPreimageContractId({
174
+ networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
175
+ contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
176
+ new xdr.ContractIdPreimageFromAddress({
177
+ address: Address.fromString(this.factoryContractId).toScAddress(),
178
+ salt: hash(keyIdBuffer),
179
+ })
180
+ )
181
+ })
182
+ ).toXDR()));
183
+
184
+ // attempt passkey id derivation
185
+ try {
186
+ await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
187
+ }
188
+ // if that fails look up from the factory mapper
189
+ catch {
190
+ const { val } = await this.rpc.getContractData(this.factoryContractId, xdr.ScVal.scvBytes(keyIdBuffer))
191
+ contractId = scValToNative(val.contractData().val())
192
+ }
193
+
194
+ this.wallet = new PasskeyClient({
195
+ contractId,
196
+ networkPassphrase: this.networkPassphrase,
197
+ rpcUrl: this.rpcUrl
198
+ })
199
+
200
+ // get and set the sudo signer
201
+ await this.getData()
202
+
203
+ return {
204
+ keyId: keyIdBuffer,
205
+ contractId
206
+ }
207
+ }
208
+
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,
216
+ options?: {
217
+ keyId?: 'any' | 'sudo' | string | Uint8Array
218
+ ledgersToLive?: number
219
+ }
220
+ ) {
221
+ // Default mirrors DEFAULT_TIMEOUT (currently 5 minutes) https://github.com/stellar/js-stellar-sdk/blob/master/src/contract/utils.ts#L7
222
+ let { keyId, ledgersToLive = 60 } = options || {}
223
+
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
+ 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()
251
+ )
252
+
253
+ const authenticationResponse = await startAuthentication(
254
+ keyId === 'any'
255
+ || (keyId === 'sudo' && !this.sudoKeyId)
256
+ || (!keyId && !this.keyId)
257
+ ? {
258
+ challenge: base64url(authHash),
259
+ // rpId: undefined,
260
+ userVerification: "discouraged",
261
+ }
262
+ : {
263
+ challenge: base64url(authHash),
264
+ // rpId: undefined,
265
+ allowCredentials: [
266
+ {
267
+ id: keyId === 'sudo'
268
+ ? this.sudoKeyId!
269
+ : keyId instanceof Uint8Array
270
+ ? base64url(keyId)
271
+ : keyId || this.keyId!,
272
+ type: "public-key",
273
+ },
274
+ ],
275
+ userVerification: "discouraged",
276
+ }
277
+ );
278
+
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
+ const signatureRaw = base64url.toBuffer(authenticationResponse.response.signature);
287
+ const signature = this.convertEcdsaSignatureAsnToCompact(signatureRaw);
288
+ const creds = auth.credentials().address();
289
+
290
+ creds.signatureExpirationLedger(lastLedger + ledgersToLive)
291
+ creds.signature(xdr.ScVal.scvMap([
292
+ new xdr.ScMapEntry({
293
+ key: xdr.ScVal.scvSymbol('authenticator_data'),
294
+ val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.authenticatorData)),
295
+ }),
296
+ new xdr.ScMapEntry({
297
+ key: xdr.ScVal.scvSymbol('client_data_json'),
298
+ val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.clientDataJSON)),
299
+ }),
300
+ new xdr.ScMapEntry({
301
+ key: xdr.ScVal.scvSymbol('id'),
302
+ val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.id)),
303
+ }),
304
+ new xdr.ScMapEntry({
305
+ key: xdr.ScVal.scvSymbol('signature'),
306
+ val: xdr.ScVal.scvBytes(signature),
307
+ }),
308
+ ]))
309
+
310
+ const sim = await this.rpc.simulateTransaction(txn)
311
+
312
+ if (
313
+ SorobanRpc.Api.isSimulationError(sim)
314
+ || SorobanRpc.Api.isSimulationRestore(sim) // TODO handle state archival
315
+ ) throw sim
316
+
317
+ return SorobanRpc.assembleTransaction(txn, sim).build().toXDR()
318
+ }
319
+
320
+ public async getData() {
321
+ const data: Map<string, any> = new Map()
322
+
323
+ const { val } = await this.rpc.getContractData(
324
+ this.wallet!.options.contractId,
325
+ xdr.ScVal.scvLedgerKeyContractInstance(),
326
+ );
327
+
328
+ val.contractData()
329
+ .val()
330
+ .instance()
331
+ .storage()
332
+ ?.forEach((entry) => {
333
+ data.set(
334
+ scValToNative(entry.key()),
335
+ scValToNative(entry.val()),
336
+ );
337
+ });
338
+
339
+ this.sudoKeyId = base64url(data.get('sudo_sig'))
340
+
341
+ return data
342
+ }
343
+
344
+ /* TODO
345
+ - Add a getKeyInfo action to get info about a specific passkey
346
+ Specifically looking for name, type, etc. data so a user could grok what signer mapped to what passkey
347
+ @Later
348
+ */
349
+
350
+ private getPublicKeyObject(attestationObject: string) {
351
+ const { authData } = decode(base64url.toBuffer(attestationObject));
352
+ const authDataUint8Array = new Uint8Array(authData);
353
+ const authDataView = new DataView(authDataUint8Array.buffer, 0, authDataUint8Array.length);
354
+
355
+ let offset = 0;
356
+
357
+ // RP ID Hash (32 bytes)
358
+ const rpIdHash = authData.slice(offset, offset + 32);
359
+ offset += 32;
360
+
361
+ // Flags (1 byte)
362
+ const flags = authDataView.getUint8(offset);
363
+ offset += 1;
364
+
365
+ // Sign Count (4 bytes, big endian)
366
+ const signCount = authDataView.getUint32(offset, false);
367
+ offset += 4;
368
+
369
+ // Attested Credential Data, if present
370
+ if (flags & 0x40) { // Checking the AT flag
371
+ // AAGUID (16 bytes)
372
+ const aaguid = authData.slice(offset, offset + 16);
373
+ offset += 16;
374
+
375
+ // Credential ID Length (2 bytes, big endian)
376
+ const credIdLength = authDataView.getUint16(offset, false);
377
+ offset += 2;
378
+
379
+ // Credential ID (variable length)
380
+ const credentialId = authData.slice(offset, offset + credIdLength);
381
+ offset += credIdLength;
382
+
383
+ // Credential Public Key - (77 bytes...I hope)
384
+ const credentialPublicKey = authData.slice(offset, offset + 77);
385
+ offset += 77;
386
+
387
+ // Any leftover bytes. I found some when using a YubiKey
388
+ const theRest = authData.slice(offset);
389
+
390
+ // Decode the credential public key to COSE
391
+ const publicKeyObject = new Map<string, any>(Object.entries(decode(credentialPublicKey)));
392
+
393
+ return {
394
+ rpIdHash,
395
+ flags,
396
+ signCount,
397
+ aaguid,
398
+ credIdLength,
399
+ credentialId,
400
+ credentialPublicKey,
401
+ theRest,
402
+ publicKeyObject
403
+ };
404
+ }
405
+
406
+ throw new Error("Attested credential data not present in the flags.");
407
+ }
408
+
409
+ private convertEcdsaSignatureAsnToCompact(sig: Buffer) {
410
+ // Define the order of the curve secp256k1
411
+ // https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
412
+ const q = Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex')
413
+
414
+ // ASN Sequence
415
+ let offset = 0;
416
+ if (sig[offset] != 0x30) {
417
+ throw "signature is not a sequence";
418
+ }
419
+ offset += 1;
420
+
421
+ // ASN Sequence Byte Length
422
+ offset += 1;
423
+
424
+ // ASN Integer (R)
425
+ if (sig[offset] != 0x02) {
426
+ throw "first element in sequence is not an integer";
427
+ }
428
+ offset += 1;
429
+
430
+ // ASN Integer (R) Byte Length
431
+ const rLen = sig[offset];
432
+ offset += 1;
433
+
434
+ // ASN Integer (R) Byte Value
435
+ if (rLen >= 33) {
436
+ if (rLen != 33 || sig[offset] != 0x00) {
437
+ throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
438
+ }
439
+ offset += 1;
440
+ }
441
+ const r = sig.slice(offset, offset + 32);
442
+ offset += 32;
443
+
444
+ // ASN Integer (S)
445
+ if (sig[offset] != 0x02) {
446
+ throw "second element in sequence is not an integer";
447
+ }
448
+ offset += 1;
449
+
450
+ // ASN Integer (S) Byte Length
451
+ const sLen = sig[offset];
452
+ offset += 1;
453
+
454
+ // ASN Integer (S) Byte Value
455
+ if (sLen >= 33) {
456
+ if (sLen != 33 || sig[offset] != 0x00) {
457
+ throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
458
+ }
459
+ offset += 1;
460
+ }
461
+
462
+ const s = sig.slice(offset, offset + 32);
463
+
464
+ offset += 32;
465
+
466
+ let signature64: Buffer
467
+
468
+ // Force low S range
469
+ // https://github.com/stellar/stellar-protocol/discussions/1435#discussioncomment-8809175
470
+ // https://discord.com/channels/897514728459468821/1233048618571927693
471
+ if (bufToBigint(s) > ((bufToBigint(q) - BigInt(1)) / BigInt(2))) {
472
+ signature64 = Buffer.from([...r, ...Buffer.from(bigintToBuf(bufToBigint(q) - bufToBigint(s), true) as ArrayBuffer)]);
473
+ } else {
474
+ signature64 = Buffer.from([...r, ...s]);
475
+ }
476
+
477
+ return signature64;
478
+ }
479
+ }
@@ -0,0 +1,9 @@
1
+ export declare class PasskeyBase {
2
+ launchtubeUrl: string | undefined;
3
+ launchtubeJwt: string | undefined;
4
+ constructor(options: {
5
+ launchtubeUrl?: string;
6
+ launchtubeJwt?: string;
7
+ });
8
+ send(xdr: string, fee?: number): Promise<any>;
9
+ }
package/types/index.d.ts CHANGED
@@ -1,49 +1,3 @@
1
- import { Client as PasskeyClient } from 'passkey-kit-sdk';
2
- import { Client as FactoryClient } from 'passkey-factory-sdk';
3
- import { Networks, Transaction, Horizon, SorobanRpc } from '@stellar/stellar-sdk';
4
- import { Buffer } from 'buffer';
5
- export declare class PasskeyAccount {
6
- keyId: string | undefined;
7
- sudoKeyId: string | undefined;
8
- wallet: PasskeyClient | undefined;
9
- factory: FactoryClient;
10
- sequencePublicKey: string;
11
- networkPassphrase: Networks;
12
- horizonUrl: string;
13
- horizon: Horizon.Server;
14
- rpcUrl: string;
15
- rpc: SorobanRpc.Server;
16
- feeBumpUrl: string | undefined;
17
- feeBumpJwt: string | undefined;
18
- factoryContractId: string;
19
- constructor(options: {
20
- sequencePublicKey: string;
21
- networkPassphrase: Networks;
22
- horizonUrl: string;
23
- rpcUrl: string;
24
- feeBumpUrl?: string;
25
- feeBumpJwt?: string;
26
- factoryContractId?: string;
27
- });
28
- createWallet(name: string, user: string): Promise<{
29
- keyId: Buffer;
30
- contractId: string;
31
- xdr: string;
32
- }>;
33
- createKey(name: string, user: string): Promise<{
34
- keyId: Buffer;
35
- publicKey: Buffer;
36
- }>;
37
- connectWallet(id?: string): Promise<{
38
- keyId: Buffer;
39
- contractId: string;
40
- }>;
41
- sign(txn: Transaction | string, options?: {
42
- keyId?: 'any' | 'sudo' | string | Uint8Array;
43
- ledgersToLive?: number;
44
- }): Promise<string>;
45
- send(txn: Transaction, fee?: number): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
46
- getData(): Promise<Map<string, any>>;
47
- private getPublicKeyObject;
48
- private convertEcdsaSignatureAsnToCompact;
49
- }
1
+ import { PasskeyBase } from "./base";
2
+ import { PasskeyKit } from "./kit";
3
+ export { PasskeyBase, PasskeyKit };
@@ -1,28 +1,22 @@
1
1
  import { Client as PasskeyClient } from 'passkey-kit-sdk';
2
2
  import { Client as FactoryClient } from 'passkey-factory-sdk';
3
- import { Networks, Transaction, Horizon, SorobanRpc } from '@stellar/stellar-sdk';
3
+ import { Networks, Transaction, SorobanRpc } from '@stellar/stellar-sdk';
4
4
  import { Buffer } from 'buffer';
5
- export declare class PasskeyAccount {
5
+ import { PasskeyBase } from './base';
6
+ export declare class PasskeyKit extends PasskeyBase {
6
7
  keyId: string | undefined;
7
8
  sudoKeyId: string | undefined;
8
9
  wallet: PasskeyClient | undefined;
9
10
  factory: FactoryClient;
10
- sequencePublicKey: string;
11
11
  networkPassphrase: Networks;
12
- horizonUrl: string;
13
- horizon: Horizon.Server;
14
12
  rpcUrl: string;
15
13
  rpc: SorobanRpc.Server;
16
- feeBumpUrl: string | undefined;
17
- feeBumpJwt: string | undefined;
18
14
  factoryContractId: string;
19
15
  constructor(options: {
20
- sequencePublicKey: string;
21
16
  networkPassphrase: Networks;
22
- horizonUrl: string;
23
17
  rpcUrl: string;
24
- feeBumpUrl?: string;
25
- feeBumpJwt?: string;
18
+ launchtubeUrl?: string;
19
+ launchtubeJwt?: string;
26
20
  factoryContractId?: string;
27
21
  });
28
22
  createWallet(name: string, user: string): Promise<{
@@ -42,7 +36,6 @@ export declare class PasskeyAccount {
42
36
  keyId?: 'any' | 'sudo' | string | Uint8Array;
43
37
  ledgersToLive?: number;
44
38
  }): Promise<string>;
45
- send(txn: Transaction, fee?: number): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
46
39
  getData(): Promise<Map<string, any>>;
47
40
  private getPublicKeyObject;
48
41
  private convertEcdsaSignatureAsnToCompact;