From bf4619584e641598045b5bfd9e95014a24c3771b Mon Sep 17 00:00:00 2001 From: Zelimir Fedoran Date: Wed, 20 Dec 2023 13:54:14 -0500 Subject: [PATCH 1/4] wip on payments with login --- packages/client/src/intents.ts | 18 ++- packages/library/package.json | 4 +- packages/library/src/elements/options.ts | 17 ++- packages/library/src/elements/validate.ts | 57 +++++++- packages/library/src/errors.ts | 10 ++ packages/library/src/intent.ts | 18 ++- ...ent_request.ts => PaymentRequestIntent.ts} | 2 +- .../intents/PaymentRequestWithLoginIntent.ts | 137 ++++++++++++++++++ packages/library/src/intents/index.ts | 3 +- packages/library/src/keys/keypair.ts | 2 +- 10 files changed, 255 insertions(+), 13 deletions(-) rename packages/library/src/intents/{payment_request.ts => PaymentRequestIntent.ts} (99%) create mode 100644 packages/library/src/intents/PaymentRequestWithLoginIntent.ts diff --git a/packages/client/src/intents.ts b/packages/client/src/intents.ts index 398a79b..f22b3a4 100644 --- a/packages/client/src/intents.ts +++ b/packages/client/src/intents.ts @@ -3,8 +3,11 @@ import { Buffer } from "buffer"; import { IntentOptions, PaymentRequestIntent, + PaymentRequestWithLoginIntent, PaymentRequestOptions, - WebhookParams + LoginRequestOptions, + WebhookParams, + Intent, } from "@code-wallet/library"; import { Connection } from "./connection"; @@ -19,7 +22,11 @@ enum PaymentIntentState { Confirmed = 'confirmed', } -type CreatePaymentIntentOptions = IntentOptions & PaymentRequestOptions & Partial; +type CreatePaymentIntentOptions = PaymentRequestOptions & + IntentOptions & + Partial & + Partial; + type GetStatusForIntentOptions = { intent: string }; const pending = { status: PaymentIntentState.Pending }; @@ -39,7 +46,12 @@ const paymentIntents = { create: async (obj: CreatePaymentIntentOptions) => { obj.mode = 'payment'; - const intent = new PaymentRequestIntent(obj); + let intent : Intent; + if (obj.login) { + intent = new PaymentRequestWithLoginIntent(obj); + } else { + intent = new PaymentRequestIntent(obj); + } const envelope = intent.sign(); const body = { diff --git a/packages/library/package.json b/packages/library/package.json index e5c804e..0ed16f3 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@code-wallet/library", - "version": "1.0.5", + "version": "1.1.0", "license": "MIT", "repository": { "type": "git", @@ -22,7 +22,7 @@ "maintained node versions" ], "dependencies": { - "@code-wallet/rpc": "^1.0.0", + "@code-wallet/rpc": "^1.1.0", "@noble/curves": "^1.2.0", "@noble/hashes": "^1.3.0", "bs58": "^5.0.0", diff --git a/packages/library/src/elements/options.ts b/packages/library/src/elements/options.ts index 39fc68e..3083162 100644 --- a/packages/library/src/elements/options.ts +++ b/packages/library/src/elements/options.ts @@ -21,7 +21,7 @@ type ElementLocale = string; * Defines the options needed for processing a payment request. */ interface PaymentRequestOptions { - /** The destination identifier, e.g., an account or user ID. */ + /** The destination token address as a base58 string. */ destination: string; /** The monetary amount for the payment request. */ @@ -31,6 +31,19 @@ interface PaymentRequestOptions { currency: CurrencyCode; } +/** + * Defines the options needed for processing a login request from a third-party. + */ +interface LoginRequestOptions { + login: { + /** The hostname of a third-party that is requesting this login. This hostname must serve a /.well-known/code-payments.json file with the verifier public key. */ + domain: string; + + /** The public key of the verifier that sign the login request as a base58 string. This public key must be found in the well-known file. */ + verifier: string; + }; +} + /** * Describes the locale options for the element. */ @@ -53,6 +66,7 @@ interface AppearanceOptions { */ type ElementOptions = Partial & Partial & + Partial & Partial & Partial & Partial & @@ -68,4 +82,5 @@ export type { LocaleOptions, AppearanceOptions, PaymentRequestOptions, + LoginRequestOptions, }; \ No newline at end of file diff --git a/packages/library/src/elements/validate.ts b/packages/library/src/elements/validate.ts index 8fd3651..7c85e54 100644 --- a/packages/library/src/elements/validate.ts +++ b/packages/library/src/elements/validate.ts @@ -7,8 +7,13 @@ import { ErrDestinationRequired, ErrInvalidCurrency, ErrInvalidMode, + ErrLoginRequired, + ErrLoginDomainRequired, + ErrLoginVerifierRequired, + ErrNotImplemented, + ErrInvalidValue, } from '../errors'; -import { PublicKey } from '../keys'; +import { Keypair, PublicKey } from '../keys'; /** * Validates the properties of the given `ElementOptions` for intents. @@ -58,6 +63,45 @@ function validatePaymentRequestOptions(intent: ElementOptions) { PublicKey.fromBase58(intent.destination); } +/** + * Validates the properties of the given `ElementOptions` for login requests. + * + * @param intent The options to validate. + * @throws {ErrLoginRequired} If the `login` property is undefined. + * @throws {ErrLoginDomainRequired} If the `login.domain` property is undefined. + * @throws {ErrLoginVerifierRequired} If the `login.verifier` property is undefined. + * @throws {ErrInvalidAddress} If the `login.verifier` property is not a valid base58 address. + */ +function validateLoginRequestOptions(intent: ElementOptions) { + if (intent.login === undefined) { + throw ErrLoginRequired(); + } + + if (intent.login.domain === undefined) { + throw ErrLoginDomainRequired(); + } + + if (intent.login.verifier === undefined) { + throw ErrLoginVerifierRequired(); + } + + // Validate that the verifier is a valid address. + PublicKey.fromBase58(intent.login.verifier); +} + +/** + * Validates the properties of the given `ElementOptions` for signers. + */ +function validateSigners(intent: ElementOptions) { + if (!intent.signers) { return; } + + for (const signer of intent.signers) { + if (signer !instanceof Keypair) { + throw ErrInvalidValue(); + } + } +} + /** * Validates the properties of the given `ElementOptions` depending on its mode. * @@ -68,8 +112,19 @@ function validateElementOptions(intent: ElementOptions) { validateIntentOptions(intent); switch (intent.mode) { + case 'login': + validateSigners(intent); + validateLoginRequestOptions(intent); + throw ErrNotImplemented(); // TODO: implement login (soon) + break; case 'payment': validatePaymentRequestOptions(intent); + + // Payments can ask for login, so we need to validate that too. + if (intent.login) { + validateSigners(intent); + validateLoginRequestOptions(intent); + } break; default: throw ErrInvalidMode(); diff --git a/packages/library/src/errors.ts b/packages/library/src/errors.ts index d26b7fc..04577f8 100644 --- a/packages/library/src/errors.ts +++ b/packages/library/src/errors.ts @@ -1,3 +1,4 @@ +const ErrNotImplemented = () => new Error("not implemented"); const ErrInvalidSize = () => new Error("invalid size"); const ErrDestinationRequired = () => new Error("destination is required"); const ErrAmountRequired = () => new Error("amount is required"); @@ -5,10 +6,15 @@ const ErrCurrencyRequired = () => new Error("currency is required"); const ErrInvalidCurrency = () => new Error("invalid currency"); const ErrUnexpectedError = () => new Error("unexpected error"); const ErrAmbiguousNonce = () => new Error("cannot derive nonce from both clientSecret and idempotencyKey"); +const ErrInvalidValue = () => new Error("invalid value"); const ErrInvalidMode = () => new Error(`invalid mode`); const ErrInvalidAddress = () => new Error("invalid address"); +const ErrLoginRequired = () => new Error("login is required"); +const ErrLoginDomainRequired = () => new Error("login domain is required"); +const ErrLoginVerifierRequired = () => new Error("login verifier is required"); export { + ErrNotImplemented, ErrInvalidSize, ErrDestinationRequired, ErrAmountRequired, @@ -16,6 +22,10 @@ export { ErrInvalidCurrency, ErrUnexpectedError, ErrAmbiguousNonce, + ErrInvalidValue, ErrInvalidMode, ErrInvalidAddress, + ErrLoginRequired, + ErrLoginDomainRequired, + ErrLoginVerifierRequired, }; diff --git a/packages/library/src/intent.ts b/packages/library/src/intent.ts index d1af53c..667713b 100644 --- a/packages/library/src/intent.ts +++ b/packages/library/src/intent.ts @@ -1,8 +1,9 @@ import * as proto from '@code-wallet/rpc'; import { IdempotencyKey } from './idempotency'; import { ElementOptions } from './elements/options'; +import { Keypair } from './keys'; -export type IntentType = 'payment'; +export type IntentType = 'payment' | 'login'; /** * Options for creating an intent. @@ -22,6 +23,11 @@ export interface IntentOptions { * See https://code-wallet.github.io/code-sdk/docs/reference/idempotency.html for more information. */ idempotencyKey?: string; + + /** + * A list of signers for an intent. + */ + signers?: Keypair[]; } /** @@ -60,8 +66,9 @@ export interface Intent { toProto(): proto.Message; /** - * Sign this intent with the rendezvous private key, returning an intent - * that is ready to be sent to the Code Sequencer. + * Sign this intent with the signer keys (if needed), returning an intent + * that is ready to be sent to the Code Sequencer. The intent will be signed + * with the rendezvous keypair by default, so no need to pass it in. * * @returns {SignedIntent} The signed intent. */ @@ -73,6 +80,11 @@ export interface Intent { * @returns {string} The client secret for this intent. */ getClientSecret(): string; + + /** + * Get the intent ID for this intent, which can be used to create linked browser elements or request status. + */ + getIntentId(): string; } /** diff --git a/packages/library/src/intents/payment_request.ts b/packages/library/src/intents/PaymentRequestIntent.ts similarity index 99% rename from packages/library/src/intents/payment_request.ts rename to packages/library/src/intents/PaymentRequestIntent.ts index 3b39e0b..136841f 100644 --- a/packages/library/src/intents/payment_request.ts +++ b/packages/library/src/intents/PaymentRequestIntent.ts @@ -112,7 +112,7 @@ class PaymentRequestIntent implements Intent { * * @returns The protobuf representation of the payment request intent. */ - toProto() { + toProto() : proto.Message { const destination = PublicKey.fromBase58(this.options.destination!); const { currency, amount } = this.options; diff --git a/packages/library/src/intents/PaymentRequestWithLoginIntent.ts b/packages/library/src/intents/PaymentRequestWithLoginIntent.ts new file mode 100644 index 0000000..bb54c37 --- /dev/null +++ b/packages/library/src/intents/PaymentRequestWithLoginIntent.ts @@ -0,0 +1,137 @@ +import * as proto from '@code-wallet/rpc'; +import { Keypair, PublicKey } from '../keys'; +import { SignedIntent } from '../intent'; +import { + ErrLoginDomainRequired, + ErrLoginRequired, + ErrLoginVerifierRequired, + ErrUnexpectedError +} from '../errors'; +import { ElementOptions } from '../elements/options'; +import { PaymentRequestIntent } from './PaymentRequestIntent'; + +/** + * Represents a payment request with login and provides methods to construct, validate, and sign the request. + */ +class PaymentRequestWithLoginIntent extends PaymentRequestIntent { + domain: string; + verifier: PublicKey; + signer?: Keypair; + + /** + * Constructs a new PaymentRequestIntent instance. + * + * @param opt - The payment request options. + */ + constructor(opt: ElementOptions) { + super(opt); + + this.validate(); + + const { signers } = opt; + const { domain, verifier } = opt.login!; + + this.domain = domain; + this.verifier = PublicKey.fromBase58(verifier); + + if (signers) { + this.signer = signers.find((k) => k.getPublicKey().toBase58() === verifier) + } + } + + /** + * Validates the payment request options. + */ + validate() { + super.validate(); + + if (!this.options.login) { + throw ErrLoginRequired(); + } + + if (!this.options.login.domain) { + throw ErrLoginDomainRequired(); + } + + if (!this.options.login.verifier) { + throw ErrLoginVerifierRequired(); + } + } + + /** + * Converts the payment request intent to its protobuf representation. + * Specifically, this method adds the domain and verifier to the request to + * receive bill message. + * + * @returns The protobuf representation of the payment request intent. + */ + toProto() : proto.Message { + const msg = super.toProto(); + const req = msg.kind.value as proto.RequestToReceiveBill; + if (!req) { + throw ErrUnexpectedError(); + } + + req.domain = new proto.Common.Domain({ + value: this.domain, + }); + + req.verifier = new proto.Common.SolanaAccountId({ + value: this.verifier.toBuffer(), + }); + + return new proto.Message({ + kind: { + case: 'requestToReceiveBill', + value: req, + } + }); + } + + /** + * Signs the payment request intent. + * + * @returns A signed intent containing the message, intent, and signature. + */ + sign(): SignedIntent { + if (!this.signer) { + throw ErrUnexpectedError(); + } + + const msg = this.toProto(); + const req = msg.kind.value as proto.RequestToReceiveBill; + if (!req) { + throw ErrUnexpectedError(); + } + + // Add the signature to the request to receive bill message. + req.signature = new proto.Common.Signature({ + value: this.signer.sign(req.toBinary()), + }); + + // Sign the message envelope with the rendezvous keypair + return super.sign(); + } + + /** + * Retrieves the client secret. + * + * @returns The client secret as a string. + */ + getClientSecret(): string { + return this.nonce.toString(); + } + + /** + * Retrieves the intent ID. + * + * @returns The intent ID as a Base58 encoded string. + */ + getIntentId(): string { + return this.rendezvousKeypair.getPublicKey().toBase58(); + } +} + +export { + PaymentRequestWithLoginIntent, +} \ No newline at end of file diff --git a/packages/library/src/intents/index.ts b/packages/library/src/intents/index.ts index 78f023b..3e56d7b 100644 --- a/packages/library/src/intents/index.ts +++ b/packages/library/src/intents/index.ts @@ -1 +1,2 @@ -export * from './payment_request'; \ No newline at end of file +export * from './PaymentRequestIntent'; +export * from './PaymentRequestWithLoginIntent'; \ No newline at end of file diff --git a/packages/library/src/keys/keypair.ts b/packages/library/src/keys/keypair.ts index 9fb26e2..5706c1f 100644 --- a/packages/library/src/keys/keypair.ts +++ b/packages/library/src/keys/keypair.ts @@ -5,8 +5,8 @@ import { PublicKey } from "./publickey"; * Represents a cryptographic key pair containing a private and public key. */ class Keypair { - privateKey: Uint8Array; publicKey: Uint8Array; + privateKey: Uint8Array; /** * Constructs a new Keypair instance. From 78b797ace4196910a07fd915b90c8e2b2fdd7670 Mon Sep 17 00:00:00 2001 From: Zelimir Fedoran Date: Wed, 20 Dec 2023 16:44:37 -0500 Subject: [PATCH 2/4] wip test cases for payment request with login --- packages/library/src/elements/validate.ts | 9 +- .../intents/PaymentRequestWithLoginIntent.ts | 17 +- .../test/payment_request_with_login.test.ts | 187 ++++++++++++++++++ 3 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 packages/library/test/payment_request_with_login.test.ts diff --git a/packages/library/src/elements/validate.ts b/packages/library/src/elements/validate.ts index 7c85e54..3f05f01 100644 --- a/packages/library/src/elements/validate.ts +++ b/packages/library/src/elements/validate.ts @@ -96,7 +96,7 @@ function validateSigners(intent: ElementOptions) { if (!intent.signers) { return; } for (const signer of intent.signers) { - if (signer !instanceof Keypair) { + if (!(signer instanceof Keypair)) { throw ErrInvalidValue(); } } @@ -113,18 +113,17 @@ function validateElementOptions(intent: ElementOptions) { switch (intent.mode) { case 'login': - validateSigners(intent); - validateLoginRequestOptions(intent); throw ErrNotImplemented(); // TODO: implement login (soon) break; case 'payment': validatePaymentRequestOptions(intent); - // Payments can ask for login, so we need to validate that too. if (intent.login) { - validateSigners(intent); validateLoginRequestOptions(intent); } + if (intent.signers) { + validateSigners(intent); + } break; default: throw ErrInvalidMode(); diff --git a/packages/library/src/intents/PaymentRequestWithLoginIntent.ts b/packages/library/src/intents/PaymentRequestWithLoginIntent.ts index bb54c37..e8a50f3 100644 --- a/packages/library/src/intents/PaymentRequestWithLoginIntent.ts +++ b/packages/library/src/intents/PaymentRequestWithLoginIntent.ts @@ -80,6 +80,10 @@ class PaymentRequestWithLoginIntent extends PaymentRequestIntent { value: this.verifier.toBuffer(), }); + req.rendezvousKey = new proto.Common.SolanaAccountId({ + value: this.rendezvousKeypair.getPublicKey().toBuffer(), + }); + return new proto.Message({ kind: { case: 'requestToReceiveBill', @@ -104,13 +108,20 @@ class PaymentRequestWithLoginIntent extends PaymentRequestIntent { throw ErrUnexpectedError(); } - // Add the signature to the request to receive bill message. req.signature = new proto.Common.Signature({ value: this.signer.sign(req.toBinary()), }); - // Sign the message envelope with the rendezvous keypair - return super.sign(); + const sig = this.rendezvousKeypair.sign(msg.toBinary()); + const intent = this.rendezvousKeypair.getPublicKey().toBase58(); + const message = msg.toBinary(); + const signature = sig; + + return { + message, + intent, + signature, + } } /** diff --git a/packages/library/test/payment_request_with_login.test.ts b/packages/library/test/payment_request_with_login.test.ts new file mode 100644 index 0000000..3b53ab0 --- /dev/null +++ b/packages/library/test/payment_request_with_login.test.ts @@ -0,0 +1,187 @@ +import { expect } from 'chai'; +import { + CurrencyCode, + ErrLoginDomainRequired, + ErrLoginRequired, + ErrLoginVerifierRequired, + IntentType, + Keypair, + PaymentRequestWithLoginIntent, + PublicKey, +} from '../src'; + +describe('PaymentRequestWithLoginIntent', () => { + + const destination = 'CYbMQjhhFwE9NxYk91582ii4Q9jexXEtTesFmsgqKWRa'; + const opt = { + destination, + amount: 0.5, + mode: 'payment' as IntentType, + currency: 'usd' as CurrencyCode, + } + + const domain = 'app.getcode.com'; + const verifier = Keypair.fromRawPrivateKey(new Uint8Array ([ + 31, 198, 32, 30, 134, 217, 253, 202, + 191, 201, 72, 101, 85, 57, 128, 211, + 204, 140, 82, 80, 37, 240, 241, 62, + 144, 107, 81, 63, 236, 197, 103, 45 + ])); + + const rendezvous = Keypair.fromSecretKey(new Uint8Array([ + 21, 17, 247, 182, 187, 209, 72, 224, + 155, 234, 125, 157, 197, 64, 106, 229, + 230, 5, 176, 18, 30, 47, 210, 243, + 87, 206, 0, 3, 208, 130, 81, 174 + ])); + + describe('constructor', () => { + it('should initialize correctly', () => { + const intent = new PaymentRequestWithLoginIntent({ + ...opt, + login: { + domain, + verifier: verifier.getPublicKey().toBase58() + } + }); + + expect(intent.options.destination).to.equal(destination); + expect(intent.options.amount).to.equal(0.5); + expect(intent.options.currency).to.equal('usd'); + expect(intent.convertedAmount).to.equal(0.5 * 100); + expect(intent.options.login!.domain).to.equal('app.getcode.com'); + expect(intent.options.login!.verifier).to.equal(verifier.getPublicKey().toBase58()); + }); + }); + + describe('validate', () => { + + it('should throw an error if login is missing', () => { + expect(() => new PaymentRequestWithLoginIntent({ + ...opt, + } as any)).to.throw(ErrLoginRequired().message); + }); + + it('should throw an error if login.domain is missing', () => { + expect(() => new PaymentRequestWithLoginIntent({ + ...opt, + login: { + verifier: verifier.getPublicKey().toBase58() + } + } as any)).to.throw(ErrLoginDomainRequired().message); + }); + + it('should throw an error if login.verifier is missing', () => { + expect(() => new PaymentRequestWithLoginIntent({ + ...opt, + login: { domain } + } as any)).to.throw(ErrLoginVerifierRequired().message); + }); + }); + + describe('toProto', () => { + it('should return correct protobuf json', () => { + const intent = new PaymentRequestWithLoginIntent({ + ...opt, + login: { + domain, + verifier: verifier.getPublicKey().toBase58() + } + }); + + intent.rendezvousKeypair = rendezvous; + const protoMessage = intent.toProto(); + const buf = protoMessage.toJson(); + + expect(buf).to.deep.equal({ + requestToReceiveBill: { + requestorAccount: { + value: PublicKey.fromBase58(destination).toBuffer().toString('base64') + }, + partial: { + currency: 'usd', + nativeAmount: 0.5, + }, + domain: { + value: domain + }, + verifier: { + value: verifier.getPublicKey().toBuffer().toString('base64') + }, + rendezvousKey: { + value: rendezvous.getPublicKey().toBuffer().toString('base64') + } + + } + }); + }); + + it('should return correct protobuf bytes', () => { + const intent = new PaymentRequestWithLoginIntent({ + ...opt, + login: { + domain, + verifier: verifier.getPublicKey().toBase58() + } + }); + + intent.rendezvousKeypair = rendezvous; + const protoMessage = intent.toProto(); + const actual = protoMessage.toBinary(); + + const expected = new Uint8Array([ + 0x2a, 0x8f, 0x01, 0x0a, 0x22, 0x0a, 0x20, 0xab, 0x88, 0x67, + 0x2f, 0x94, 0x4e, 0xa4, 0x5b, 0x3c, 0x25, 0xc2, 0x6d, 0x73, + 0x2d, 0x2e, 0x5e, 0x40, 0xd5, 0xc7, 0xc1, 0x62, 0xc3, 0xcd, + 0x68, 0x58, 0xd5, 0xc9, 0x5a, 0x23, 0xfa, 0x34, 0x55, 0x1a, + 0x0e, 0x0a, 0x03, 0x75, 0x73, 0x64, 0x11, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xe0, 0x3f, 0x22, 0x11, 0x0a, 0x0f, 0x61, + 0x70, 0x70, 0x2e, 0x67, 0x65, 0x74, 0x63, 0x6f, 0x64, 0x65, + 0x2e, 0x63, 0x6f, 0x6d, 0x2a, 0x22, 0x0a, 0x20, 0x90, 0x5c, + 0xc7, 0x96, 0xae, 0x7a, 0x19, 0x98, 0x46, 0x18, 0x36, 0xcd, + 0x9f, 0x59, 0x05, 0x2f, 0x8a, 0x2a, 0x52, 0xcd, 0x53, 0x9b, + 0x41, 0x7e, 0x57, 0x7c, 0x11, 0x82, 0x83, 0xd2, 0xa0, 0x6c, + 0x3a, 0x22, 0x0a, 0x20, 0x70, 0xa4, 0xae, 0xb6, 0x8f, 0x76, + 0x36, 0x3f, 0x22, 0x66, 0x81, 0xdf, 0x16, 0x62, 0xbf, 0xc0, + 0xf6, 0x42, 0x36, 0xe9, 0x7a, 0x7a, 0x64, 0x87, 0x46, 0x6a, + 0x93, 0x99, 0x54, 0x5e, 0x7a, 0xfb + ]); + + expect(actual.toString()).to.equal(expected.toString()); + }); + }); + + describe('sign', () => { + it('should return correct signature bytes', () => { + const intent = new PaymentRequestWithLoginIntent({ + ...opt, + login: { + domain, + verifier: verifier.getPublicKey().toBase58() + }, + signers: [verifier] + }); + + intent.rendezvousKeypair = rendezvous; + + const actual = intent.sign(); + + // TODO: Fix this test + + /* + const buf = Buffer.from('CtYBKtMBCiIKIKuIZy+UTqRbPCXCbXMtLl5A1cfBYsPNaFjVyVoj+jRVIhEKD2FwcC5nZXRjb2RlLmNvbSoiCiCQXMeWrnoZmEYYNs2fWQUviipSzVObQX5XfBGCg9KgbDJCCkBgpVkQnlTv9ackQCHPV39NBCHOKh0N5n8gSwQ7Hz8nFldMcdI+TbF+9foOcW/0g+DSnR5kbxbRYEWuRTKo5O8BOiIKIHCkrraPdjY/ImaB3xZiv8D2Qjbpenpkh0Zqk5lUXnr7Gg4KA3VzZBEAAAAAAADgPxIiCiBwpK62j3Y2PyJmgd8WYr/A9kI26Xp6ZIdGapOZVF56+xpCCkDU3CnRyHQ4w0O5D5eIqizAoaBwDft+RjWsGl+Wzo+jCGyE7u+Siw4uZT7U4VcLV6lcsfe9XeB66E7RYlmwAv0I', 'base64') + const req = proto.SendMessageRequest.fromBinary(buf); + const val = req.message?.kind.value as proto.RequestToReceiveBill; + + console.log('expected', req.message?.toJson()); + //console.log('expected bin', Buffer.from(req.message?.toBinary()!).toString('hex')); + //console.log('expected ren', Buffer.from(req.signature?.value!).toString('hex')); + //console.log('expected sig', Buffer.from(val.signature?.value!).toString('hex')); + + expect(actual.intent).to.equal(expected.intent); + expect(actual.message.toString()).to.equal(expected.message.toString()); + expect(actual.signature.toString()).to.equal(expected.signautre.toString()); + */ + }); + }); +}); \ No newline at end of file From 8a9af57ba55940ed2f7d6405906522cc6680bdeb Mon Sep 17 00:00:00 2001 From: Zelimir Fedoran Date: Tue, 9 Jan 2024 10:51:38 -0500 Subject: [PATCH 3/4] added test cases for signature --- .../intents/PaymentRequestWithLoginIntent.ts | 2 +- .../test/payment_request_with_login.test.ts | 108 +++++++++++++++--- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/packages/library/src/intents/PaymentRequestWithLoginIntent.ts b/packages/library/src/intents/PaymentRequestWithLoginIntent.ts index e8a50f3..539703a 100644 --- a/packages/library/src/intents/PaymentRequestWithLoginIntent.ts +++ b/packages/library/src/intents/PaymentRequestWithLoginIntent.ts @@ -99,7 +99,7 @@ class PaymentRequestWithLoginIntent extends PaymentRequestIntent { */ sign(): SignedIntent { if (!this.signer) { - throw ErrUnexpectedError(); + throw ErrLoginVerifierRequired(); } const msg = this.toProto(); diff --git a/packages/library/test/payment_request_with_login.test.ts b/packages/library/test/payment_request_with_login.test.ts index 3b53ab0..eebe9f4 100644 --- a/packages/library/test/payment_request_with_login.test.ts +++ b/packages/library/test/payment_request_with_login.test.ts @@ -1,3 +1,4 @@ +import * as proto from '@code-wallet/rpc'; import { expect } from 'chai'; import { CurrencyCode, @@ -28,7 +29,7 @@ describe('PaymentRequestWithLoginIntent', () => { 144, 107, 81, 63, 236, 197, 103, 45 ])); - const rendezvous = Keypair.fromSecretKey(new Uint8Array([ + const rendezvous = Keypair.fromRawPrivateKey(new Uint8Array([ 21, 17, 247, 182, 187, 209, 72, 224, 155, 234, 125, 157, 197, 64, 106, 229, 230, 5, 176, 18, 30, 47, 210, 243, @@ -90,10 +91,9 @@ describe('PaymentRequestWithLoginIntent', () => { }); intent.rendezvousKeypair = rendezvous; - const protoMessage = intent.toProto(); - const buf = protoMessage.toJson(); + const msg = intent.toProto(); - expect(buf).to.deep.equal({ + expect(msg.toJson()).to.deep.equal({ requestToReceiveBill: { requestorAccount: { value: PublicKey.fromBase58(destination).toBuffer().toString('base64') @@ -152,7 +152,25 @@ describe('PaymentRequestWithLoginIntent', () => { }); describe('sign', () => { - it('should return correct signature bytes', () => { + it('should expect a login signer', () => { + const intent = new PaymentRequestWithLoginIntent({ + ...opt, + login: { + domain, + verifier: verifier.getPublicKey().toBase58() + }, + }); + + intent.rendezvousKeypair = rendezvous; + + try { + intent.sign(); + } catch (e) { + expect(e.message).to.equal(ErrLoginVerifierRequired().message); + } + }); + + it('should return correct signature json', () => { const intent = new PaymentRequestWithLoginIntent({ ...opt, login: { @@ -164,24 +182,76 @@ describe('PaymentRequestWithLoginIntent', () => { intent.rendezvousKeypair = rendezvous; - const actual = intent.sign(); + const res = intent.sign(); + const msg = proto.Message.fromBinary(res.message); + + expect(msg.toJson()).to.deep.equal({ + requestToReceiveBill: { + requestorAccount: { + value: PublicKey.fromBase58(destination).toBuffer().toString('base64') + }, + partial: { + currency: 'usd', + nativeAmount: 0.5, + }, + domain: { + value: domain + }, + verifier: { + value: verifier.getPublicKey().toBuffer().toString('base64') + }, + rendezvousKey: { + value: rendezvous.getPublicKey().toBuffer().toString('base64') + }, + signature: { + value: '7EdpP8TSajVJ+79X0yCmG5FAlIlpJEO8Qsvo4CuSwyOLsJMXMqb15TrVyrFiNINEYHWfxpzJvwNkvrdiZT74Dg==' + }, + } + }); + }); + + it('should return correct signature bytes', () => { + const intent = new PaymentRequestWithLoginIntent({ + ...opt, + login: { + domain, + verifier: verifier.getPublicKey().toBase58() + }, + signers: [verifier] + }); - // TODO: Fix this test + intent.rendezvousKeypair = rendezvous; - /* - const buf = Buffer.from('CtYBKtMBCiIKIKuIZy+UTqRbPCXCbXMtLl5A1cfBYsPNaFjVyVoj+jRVIhEKD2FwcC5nZXRjb2RlLmNvbSoiCiCQXMeWrnoZmEYYNs2fWQUviipSzVObQX5XfBGCg9KgbDJCCkBgpVkQnlTv9ackQCHPV39NBCHOKh0N5n8gSwQ7Hz8nFldMcdI+TbF+9foOcW/0g+DSnR5kbxbRYEWuRTKo5O8BOiIKIHCkrraPdjY/ImaB3xZiv8D2Qjbpenpkh0Zqk5lUXnr7Gg4KA3VzZBEAAAAAAADgPxIiCiBwpK62j3Y2PyJmgd8WYr/A9kI26Xp6ZIdGapOZVF56+xpCCkDU3CnRyHQ4w0O5D5eIqizAoaBwDft+RjWsGl+Wzo+jCGyE7u+Siw4uZT7U4VcLV6lcsfe9XeB66E7RYlmwAv0I', 'base64') - const req = proto.SendMessageRequest.fromBinary(buf); - const val = req.message?.kind.value as proto.RequestToReceiveBill; + const res = intent.sign(); + const msg = proto.Message.fromBinary(res.message); + const actual = msg.toBinary(); - console.log('expected', req.message?.toJson()); - //console.log('expected bin', Buffer.from(req.message?.toBinary()!).toString('hex')); - //console.log('expected ren', Buffer.from(req.signature?.value!).toString('hex')); - //console.log('expected sig', Buffer.from(val.signature?.value!).toString('hex')); + const expected = new Uint8Array([ + 0x2a, 0xd3, 0x01, 0x0a, 0x22, 0x0a, 0x20, 0xab, 0x88, 0x67, + 0x2f, 0x94, 0x4e, 0xa4, 0x5b, 0x3c, 0x25, 0xc2, 0x6d, 0x73, + 0x2d, 0x2e, 0x5e, 0x40, 0xd5, 0xc7, 0xc1, 0x62, 0xc3, 0xcd, + 0x68, 0x58, 0xd5, 0xc9, 0x5a, 0x23, 0xfa, 0x34, 0x55, 0x1a, + 0x0e, 0x0a, 0x03, 0x75, 0x73, 0x64, 0x11, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xe0, 0x3f, 0x22, 0x11, 0x0a, 0x0f, 0x61, + 0x70, 0x70, 0x2e, 0x67, 0x65, 0x74, 0x63, 0x6f, 0x64, 0x65, + 0x2e, 0x63, 0x6f, 0x6d, 0x2a, 0x22, 0x0a, 0x20, 0x90, 0x5c, + 0xc7, 0x96, 0xae, 0x7a, 0x19, 0x98, 0x46, 0x18, 0x36, 0xcd, + 0x9f, 0x59, 0x05, 0x2f, 0x8a, 0x2a, 0x52, 0xcd, 0x53, 0x9b, + 0x41, 0x7e, 0x57, 0x7c, 0x11, 0x82, 0x83, 0xd2, 0xa0, 0x6c, + 0x32, 0x42, 0x0a, 0x40, 0xec, 0x47, 0x69, 0x3f, 0xc4, 0xd2, + 0x6a, 0x35, 0x49, 0xfb, 0xbf, 0x57, 0xd3, 0x20, 0xa6, 0x1b, + 0x91, 0x40, 0x94, 0x89, 0x69, 0x24, 0x43, 0xbc, 0x42, 0xcb, + 0xe8, 0xe0, 0x2b, 0x92, 0xc3, 0x23, 0x8b, 0xb0, 0x93, 0x17, + 0x32, 0xa6, 0xf5, 0xe5, 0x3a, 0xd5, 0xca, 0xb1, 0x62, 0x34, + 0x83, 0x44, 0x60, 0x75, 0x9f, 0xc6, 0x9c, 0xc9, 0xbf, 0x03, + 0x64, 0xbe, 0xb7, 0x62, 0x65, 0x3e, 0xf8, 0x0e, 0x3a, 0x22, + 0x0a, 0x20, 0x70, 0xa4, 0xae, 0xb6, 0x8f, 0x76, 0x36, 0x3f, + 0x22, 0x66, 0x81, 0xdf, 0x16, 0x62, 0xbf, 0xc0, 0xf6, 0x42, + 0x36, 0xe9, 0x7a, 0x7a, 0x64, 0x87, 0x46, 0x6a, 0x93, 0x99, + 0x54, 0x5e, 0x7a, 0xfb + ]); - expect(actual.intent).to.equal(expected.intent); - expect(actual.message.toString()).to.equal(expected.message.toString()); - expect(actual.signature.toString()).to.equal(expected.signautre.toString()); - */ + expect(actual.toString()).to.equal(expected.toString()); }); }); }); \ No newline at end of file From 028c6889d367e67cffe496c184bafbdc00ec2b2f Mon Sep 17 00:00:00 2001 From: Zelimir Fedoran Date: Tue, 9 Jan 2024 12:47:41 -0500 Subject: [PATCH 4/4] version bump --- package-lock.json | 14 +++++++------- packages/client/package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 295dfb9..0d19cf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3906,7 +3906,7 @@ "version": "1.0.4", "license": "MIT", "dependencies": { - "@code-wallet/library": "^1.0.4", + "@code-wallet/library": "^1.1.0", "bs58": "^5.0.0", "buffer": "6.0.3" }, @@ -3972,10 +3972,10 @@ }, "packages/library": { "name": "@code-wallet/library", - "version": "1.0.4", + "version": "1.1.0", "license": "MIT", "dependencies": { - "@code-wallet/rpc": "^1.0.0", + "@code-wallet/rpc": "^1.1.0", "@noble/curves": "^1.2.0", "@noble/hashes": "^1.3.0", "bs58": "^5.0.0", @@ -3994,7 +3994,7 @@ }, "packages/mnemonic": { "name": "@code-wallet/mnemonic", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "dependencies": { "@code-wallet/library": "^1.0.0", @@ -4037,7 +4037,7 @@ }, "packages/rpc": { "name": "@code-wallet/rpc", - "version": "1.0.2", + "version": "1.1.0", "license": "MIT", "dependencies": { "@bufbuild/connect": "^0.8.6", @@ -4098,7 +4098,7 @@ "@code-wallet/client": { "version": "file:packages/client", "requires": { - "@code-wallet/library": "^1.0.4", + "@code-wallet/library": "^1.1.0", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", "@types/node": "^20.5.7", @@ -4155,7 +4155,7 @@ "@code-wallet/library": { "version": "file:packages/library", "requires": { - "@code-wallet/rpc": "^1.0.0", + "@code-wallet/rpc": "^1.1.0", "@noble/curves": "^1.2.0", "@noble/hashes": "^1.3.0", "@types/chai": "^4.3.5", diff --git a/packages/client/package.json b/packages/client/package.json index f32f704..92862e8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@code-wallet/client", - "version": "1.0.4", + "version": "1.1.0", "license": "MIT", "repository": { "type": "git", @@ -22,7 +22,7 @@ "maintained node versions" ], "dependencies": { - "@code-wallet/library": "^1.0.4", + "@code-wallet/library": "^1.1.0", "bs58": "^5.0.0", "buffer": "6.0.3" },