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" }, 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..3f05f01 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,18 @@ function validateElementOptions(intent: ElementOptions) { validateIntentOptions(intent); switch (intent.mode) { + case 'login': + throw ErrNotImplemented(); // TODO: implement login (soon) + break; case 'payment': validatePaymentRequestOptions(intent); + + if (intent.login) { + validateLoginRequestOptions(intent); + } + if (intent.signers) { + validateSigners(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..539703a --- /dev/null +++ b/packages/library/src/intents/PaymentRequestWithLoginIntent.ts @@ -0,0 +1,148 @@ +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(), + }); + + req.rendezvousKey = new proto.Common.SolanaAccountId({ + value: this.rendezvousKeypair.getPublicKey().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 ErrLoginVerifierRequired(); + } + + const msg = this.toProto(); + const req = msg.kind.value as proto.RequestToReceiveBill; + if (!req) { + throw ErrUnexpectedError(); + } + + req.signature = new proto.Common.Signature({ + value: this.signer.sign(req.toBinary()), + }); + + const sig = this.rendezvousKeypair.sign(msg.toBinary()); + const intent = this.rendezvousKeypair.getPublicKey().toBase58(); + const message = msg.toBinary(); + const signature = sig; + + return { + message, + intent, + signature, + } + } + + /** + * 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. 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..eebe9f4 --- /dev/null +++ b/packages/library/test/payment_request_with_login.test.ts @@ -0,0 +1,257 @@ +import * as proto from '@code-wallet/rpc'; +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.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, + 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 msg = intent.toProto(); + + 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') + } + + } + }); + }); + + 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 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: { + domain, + verifier: verifier.getPublicKey().toBase58() + }, + signers: [verifier] + }); + + intent.rendezvousKeypair = rendezvous; + + 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] + }); + + intent.rendezvousKeypair = rendezvous; + + const res = intent.sign(); + const msg = proto.Message.fromBinary(res.message); + const actual = msg.toBinary(); + + 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.toString()).to.equal(expected.toString()); + }); + }); +}); \ No newline at end of file