diff --git a/.gitignore b/.gitignore index 647adcd0..124b612a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ vitest.config.*.timestamp* packages/*/dist storybook-static + +docs/superpowers/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 33830699..75155343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## 0.7.9 (2026-05-15) + +### 🚀 Features + +- **host-api-wrapper:** the product SDK has been renamed — `@novasamatech/product-sdk` is now published as `@novasamatech/host-api-wrapper`. The name better reflects what the package actually is: a thin, ergonomic wrapper around the Host API for products to call. The public API is otherwise unchanged; the only thing consumers need to do is update their `package.json` dependency and their imports. +- **host-papp:** user attestation has moved off the Host and onto the paired Polkadot Mobile app. The Host no longer drives the attestation flow during sign-in — the mobile app handles it end-to-end as part of pairing. +- **host-papp:** `UserSession` gains a `createTransaction(payload)` method. The Host can now delegate product-account transaction signing to the paired Polkadot Mobile app via the new `CreateTransactionRequest` / `CreateTransactionResponse` SSO message pair (legacy-account signing stays Host-local). +- **host-api-wrapper:** new top-level `accounts` singleton (`createAccountsProvider()` with the default sandbox transport) for products that don't need a custom transport. +- **host-api-wrapper:** export `ProductAccountId` and `LegacyAccount` types. +- **host-api-wrapper:** `getProductAccountSigner` accepts an optional second argument selecting how the returned signer should sign transactions — `'createTransaction'` (default, new behavior) routes through `host_create_transaction` and returns the full signed extrinsic; `'signPayload'` keeps the legacy path through `host_sign_payload`, giving products that haven't migrated yet a way to opt back into the old behavior without pinning the previous SDK version. +- **host-api / host-api-wrapper:** products can now schedule a push notification for a future time, not just send one right away. Pass `scheduledAt` (a UTC timestamp in milliseconds) when calling `notificationManager.push(...)`, and the host will deliver it at that moment. Leave it out to deliver immediately as before. +- **host-api / host-api-wrapper:** `push(...)` now returns an id you can hold onto, and the new `notificationManager.cancel(id)` lets a product cancel a notification it scheduled earlier — handy for "remind me in an hour" style flows where the user changes their mind. +- **host-api / host-api-wrapper:** if the host can't accept any more scheduled notifications, the product now gets a clear `ScheduleLimitReached` error instead of a generic failure, so it can tell the user what happened. +- **host-api / host-container / host-papp:** experimental debug hooks for observing host ↔ product traffic and internal SSO state. `onHostApiDebugMessage` (from `host-container`) emits every decoded message across all containers in the process, annotated with `productId`. `onHostPappDebugMessage` (from `host-papp/debug`) emits attestation, auth, and session-lifecycle events. Both are lazy — when no subscriber is attached, the underlying decode/emit work is skipped, so leaving the hooks in code costs nothing in production. + +### 🩹 Fixes + +- **host-papp-react-ui:** pairing QR code now renders on `` via the lighter `qrcode` dep (replacing `qr-code-styling`), with pixel-snapped modules and circular finder eyes — scans reliably across phone cameras and stays crisp on hi-DPI displays. +- **host-papp-react-ui:** pairing modal/popover is responsive — shrinks to fit narrow viewports instead of being pinned at 350px. + +### ⚠️ Breaking Changes + +- **host-api-wrapper:** package was renamed from `@novasamatech/product-sdk`. Update your dependency and imports — there is no compatibility re-export under the old name. +- **host-api:** `host_create_transaction` no longer takes a separate `account_id` parameter — the account is now part of the payload as a typed `signer` field. +- **host-api:** `TxPayloadV1` got new structure that is not compatable with old one. +- **host-api-wrapper:** `getProductAccountSigner` now returns a `PolkadotSigner` whose `signTx` routes through `host_create_transaction` and returns the full signed extrinsic; `signBytes` routes through `host_sign_raw`. Previously `signTx` called `host_sign_payload` and returned a detached signature via `getPolkadotSignerFromPjs`. Callers no longer need to assemble the extrinsic themselves. (Pass `'signPayload'` as the second argument to opt back into the old behavior — see the Features section.) + + > No compatibility shim. `host_create_transaction` had no production consumers and `host_create_transaction_with_legacy_account` is only reachable via `host-api-wrapper`, which is bumped in lockstep. +- **host-api:** the push-notification format changed to support scheduling and cancellation. Hosts and products must upgrade together — older clients won't be able to send notifications to a newer host (or vice versa). + +### ❤️ Thank You + +- Sergey Zhuravlev @johnthecat +- Filippo +- Yanaty +- Vitya Livshits @cuteWarmFrog + ## 0.7.8 (2026-05-08) ### 🚀 Features diff --git a/__tests__/hostApi/accounts.spec.ts b/__tests__/hostApi/accounts.spec.ts index ecf237e6..07f38644 100644 --- a/__tests__/hostApi/accounts.spec.ts +++ b/__tests__/hostApi/accounts.spec.ts @@ -9,7 +9,7 @@ import { createTransport, toHex, } from '@novasamatech/host-api'; -import type { AccountConnectionStatus, ProductAccount } from '@novasamatech/host-api-wrapper'; +import type { AccountConnectionStatus, LegacyAccount, ProductAccount } from '@novasamatech/host-api-wrapper'; import { createAccountsProvider } from '@novasamatech/host-api-wrapper'; import type { ContainerHandlerOf } from '@novasamatech/host-container'; import { createContainer } from '@novasamatech/host-container'; @@ -29,11 +29,15 @@ function setup() { } const mockPublicKey = new Uint8Array(32).fill(1); -const mockAccount: ProductAccount = { +const mockProductAccount: ProductAccount = { dotNsIdentifier: 'product.dot', derivationIndex: 0, publicKey: mockPublicKey, }; +const mockLegacyAccount: LegacyAccount = { + publicKey: mockPublicKey, + name: 'Test Account', +}; const mockRingLocation: CodecType = { genesisHash: toHex(new Uint8Array(32).fill(0x22)), @@ -95,14 +99,13 @@ describe('Host API: Accounts', () => { describe('getProductAccount', () => { it('should return account on success', async () => { const { container, accountsProvider } = setup(); - const expected = { publicKey: mockPublicKey }; - container.handleAccountGet((_, { ok }) => ok(expected)); + container.handleAccountGet((_, { ok }) => ok({ publicKey: mockPublicKey })); const result = await accountsProvider.getProductAccount('product.dot', 0); expect(result.isOk()).toBe(true); - expect(result._unsafeUnwrap()).toEqual(expected); + expect(result._unsafeUnwrap()).toEqual(mockProductAccount); }); it('should pass dotNsIdentifier and derivationIndex to handler', async () => { @@ -304,7 +307,7 @@ describe('Host API: Accounts', () => { describe('getProductAccountSigner', () => { it('should expose the correct public key', () => { const { accountsProvider } = setup(); - const signer = accountsProvider.getProductAccountSigner(mockAccount); + const signer = accountsProvider.getProductAccountSigner(mockProductAccount); expect(signer.publicKey).toEqual(mockPublicKey); }); @@ -320,11 +323,11 @@ describe('Host API: Accounts', () => { return ok({ signature: toHex(signatureBytes), signedTransaction: undefined }); }); - const signer = accountsProvider.getProductAccountSigner(mockAccount); + const signer = accountsProvider.getProductAccountSigner(mockProductAccount); const result = await signer.signBytes(rawData); expect(capturedParams).toEqual({ - account: [mockAccount.dotNsIdentifier, mockAccount.derivationIndex], + account: [mockProductAccount.dotNsIdentifier, mockProductAccount.derivationIndex], payload: { tag: 'Bytes', value: rawData }, }); expect(result).toEqual(signatureBytes); @@ -336,7 +339,7 @@ describe('Host API: Accounts', () => { container.handleSignRaw((_, { err }) => err(error)); - const signer = accountsProvider.getProductAccountSigner(mockAccount); + const signer = accountsProvider.getProductAccountSigner(mockProductAccount); await expect(signer.signBytes(new Uint8Array([1, 2, 3]))).rejects.toEqual(error); }); @@ -345,7 +348,7 @@ describe('Host API: Accounts', () => { describe('getLegacyAccountSigner', () => { it('should expose the correct public key', () => { const { accountsProvider } = setup(); - const signer = accountsProvider.getLegacyAccountSigner(mockAccount); + const signer = accountsProvider.getLegacyAccountSigner(mockLegacyAccount); expect(signer.publicKey).toEqual(mockPublicKey); }); @@ -361,7 +364,7 @@ describe('Host API: Accounts', () => { return ok({ signature: toHex(signatureBytes), signedTransaction: undefined }); }); - const signer = accountsProvider.getLegacyAccountSigner(mockAccount); + const signer = accountsProvider.getLegacyAccountSigner(mockLegacyAccount); const result = await signer.signBytes(rawData); expect(capturedParams).toMatchObject({ payload: { tag: 'Bytes', value: rawData } }); @@ -374,7 +377,7 @@ describe('Host API: Accounts', () => { container.handleSignRawWithLegacyAccount((_, { err }) => err(error)); - const signer = accountsProvider.getLegacyAccountSigner(mockAccount); + const signer = accountsProvider.getLegacyAccountSigner(mockLegacyAccount); await expect(signer.signBytes(new Uint8Array([1, 2, 3]))).rejects.toEqual(error); }); diff --git a/__tests__/hostApi/injectedWeb3Provider.spec.ts b/__tests__/hostApi/injectedWeb3Provider.spec.ts index d305d833..3922ce38 100644 --- a/__tests__/hostApi/injectedWeb3Provider.spec.ts +++ b/__tests__/hostApi/injectedWeb3Provider.spec.ts @@ -108,12 +108,12 @@ describe('Host API: injected web3 provider', () => { const response = new Uint8Array([0, 0, 1, 1]); const payload = { version: 1 as const, - signer: 'test', + signer: AccountId().dec(new Uint8Array(32)), callData: '0x0002' as const, extensions: [ { - id: 'test', - additionalSigned: '0x0000' as const, + id: 'CheckGenesis', + additionalSigned: toHex(new Uint8Array(32)), extra: '0x0000' as const, }, ], @@ -191,12 +191,12 @@ describe('Host API: injected web3 provider', () => { const payload = { version: 1 as const, - signer: 'test', + signer: AccountId().dec(new Uint8Array(32)), callData: '0x0002' as const, extensions: [ { - id: 'test', - additionalSigned: '0x0000' as const, + id: 'CheckGenesis', + additionalSigned: toHex(new Uint8Array(32)), extra: '0x0000' as const, }, ], diff --git a/__tests__/hostApi/notification.spec.ts b/__tests__/hostApi/notification.spec.ts index 08463a6f..1ba33ddf 100644 --- a/__tests__/hostApi/notification.spec.ts +++ b/__tests__/hostApi/notification.spec.ts @@ -1,4 +1,4 @@ -import { GenericError, createHostApi, createTransport, enumValue } from '@novasamatech/host-api'; +import { GenericError, PushNotificationError, createHostApi, createTransport, enumValue } from '@novasamatech/host-api'; import type { ContainerHandlerOf } from '@novasamatech/host-container'; import { createContainer } from '@novasamatech/host-container'; @@ -16,15 +16,15 @@ function setup() { } describe('Host API: PushNotification', () => { - it('should deliver a notification and gate on Notifications device permission', async () => { + it('should deliver an immediate notification and return a NotificationId', async () => { const { container, hostApi } = setup(); - const payload = { text: 'Hello, world!', deeplink: 'https://example.com/deep' }; + const payload = { text: 'Hello, world!', deeplink: 'https://example.com/deep', scheduledAt: undefined }; const devicePermissionHandler = vi.fn>( (_params, { ok }) => ok(true), ); container.handleDevicePermission(devicePermissionHandler); - const handler = vi.fn>((_, { ok }) => ok(undefined)); + const handler = vi.fn>((_, { ok }) => ok(42)); container.handlePushNotification(handler); const result = await hostApi.pushNotification(enumValue('v1', payload)); @@ -32,7 +32,7 @@ describe('Host API: PushNotification', () => { result.match( ok => { expect(ok.tag).toBe('v1'); - expect(ok.value).toBeUndefined(); + expect(ok.value).toBe(42); }, () => { throw new Error('Expected success'); @@ -47,10 +47,10 @@ describe('Host API: PushNotification', () => { it('should deliver a notification without a deeplink', async () => { const { container, hostApi } = setup(); - const payload = { text: 'Notification body', deeplink: undefined }; + const payload = { text: 'Notification body', deeplink: undefined, scheduledAt: undefined }; container.handleDevicePermission((_, { ok }) => ok(true)); - const handler = vi.fn>((_, { ok }) => ok(undefined)); + const handler = vi.fn>((_, { ok }) => ok(1)); container.handlePushNotification(handler); const result = await hostApi.pushNotification(enumValue('v1', payload)); @@ -59,15 +59,27 @@ describe('Host API: PushNotification', () => { expect(handler).toBeCalledWith(payload, { ok: expect.any(Function), err: expect.any(Function) }); }); - it('should propagate handler errors and still gate on Notifications permission', async () => { + it('should deliver a scheduled notification carrying scheduledAt as a u64', async () => { const { container, hostApi } = setup(); - const payload = { text: 'will fail', deeplink: undefined }; - const error = new GenericError({ reason: 'Delivery failed' }); + const scheduledAt = BigInt(Date.UTC(2027, 0, 1)); + const payload = { text: 'reminder', deeplink: undefined, scheduledAt }; - const devicePermissionHandler = vi.fn>( - (_params, { ok }) => ok(true), - ); - container.handleDevicePermission(devicePermissionHandler); + container.handleDevicePermission((_, { ok }) => ok(true)); + const handler = vi.fn>((_, { ok }) => ok(7)); + container.handlePushNotification(handler); + + const result = await hostApi.pushNotification(enumValue('v1', payload)); + + expect(result.isOk()).toBe(true); + expect(handler).toBeCalledWith(payload, { ok: expect.any(Function), err: expect.any(Function) }); + }); + + it('should propagate ScheduleLimitReached', async () => { + const { container, hostApi } = setup(); + const payload = { text: 'full queue', deeplink: undefined, scheduledAt: BigInt(2_000_000_000_000) }; + const error = new PushNotificationError.ScheduleLimitReached(); + + container.handleDevicePermission((_, { ok }) => ok(true)); container.handlePushNotification((_, { err }) => err(error)); const result = await hostApi.pushNotification(enumValue('v1', payload)); @@ -81,18 +93,35 @@ describe('Host API: PushNotification', () => { expect(failure.value).toEqual(error); }, ); + }); - expect(devicePermissionHandler).toHaveBeenCalledOnce(); - const [receivedPermissionParams] = devicePermissionHandler.mock.calls[0]!; - expect(receivedPermissionParams).toBe('Notifications'); + it('should propagate Unknown { reason } round-trip', async () => { + const { container, hostApi } = setup(); + const payload = { text: 'boom', deeplink: undefined, scheduledAt: undefined }; + const error = new PushNotificationError.Unknown({ reason: 'OS rejected' }); + + container.handleDevicePermission((_, { ok }) => ok(true)); + container.handlePushNotification((_, { err }) => err(error)); + + const result = await hostApi.pushNotification(enumValue('v1', payload)); + + result.match( + () => { + throw new Error('Expected failure'); + }, + failure => { + expect(failure.tag).toBe('v1'); + expect(failure.value).toEqual(error); + }, + ); }); it('should reject and skip the handler when Notifications permission is denied', async () => { const { container, hostApi } = setup(); - const payload = { text: 'blocked', deeplink: undefined }; + const payload = { text: 'blocked', deeplink: undefined, scheduledAt: undefined }; container.handleDevicePermission((_, { ok }) => ok(false)); - const handler = vi.fn>((_, { ok }) => ok(undefined)); + const handler = vi.fn>((_, { ok }) => ok(1)); container.handlePushNotification(handler); const result = await hostApi.pushNotification(enumValue('v1', payload)); @@ -104,7 +133,7 @@ describe('Host API: PushNotification', () => { }, failure => { expect(failure.tag).toBe('v1'); - expect(failure.value).toBeInstanceOf(GenericError); + expect(failure.value).toBeInstanceOf(PushNotificationError); }, ); expect(handler).not.toHaveBeenCalled(); @@ -112,10 +141,10 @@ describe('Host API: PushNotification', () => { it('should reject and skip the handler when the device permission handler errors', async () => { const { container, hostApi } = setup(); - const payload = { text: 'blocked', deeplink: undefined }; + const payload = { text: 'blocked', deeplink: undefined, scheduledAt: undefined }; container.handleDevicePermission((_, { err }) => err(new GenericError({ reason: 'permission lookup failed' }))); - const handler = vi.fn>((_, { ok }) => ok(undefined)); + const handler = vi.fn>((_, { ok }) => ok(1)); container.handlePushNotification(handler); const result = await hostApi.pushNotification(enumValue('v1', payload)); @@ -124,3 +153,55 @@ describe('Host API: PushNotification', () => { expect(handler).not.toHaveBeenCalled(); }); }); + +describe('Host API: PushNotificationCancel', () => { + it('should cancel a pending notification by id', async () => { + const { container, hostApi } = setup(); + + container.handleDevicePermission((_, { ok }) => ok(true)); + const handler = vi.fn>((_, { ok }) => + ok(undefined), + ); + container.handlePushNotificationCancel(handler); + + const result = await hostApi.pushNotificationCancel(enumValue('v1', 42)); + + expect(result.isOk()).toBe(true); + expect(handler).toBeCalledWith(42, { ok: expect.any(Function), err: expect.any(Function) }); + }); + + it('should propagate GenericError when the host returns one', async () => { + const { container, hostApi } = setup(); + const error = new GenericError({ reason: 'cancel failed' }); + + container.handleDevicePermission((_, { ok }) => ok(true)); + container.handlePushNotificationCancel((_, { err }) => err(error)); + + const result = await hostApi.pushNotificationCancel(enumValue('v1', 1)); + + result.match( + () => { + throw new Error('Expected failure'); + }, + failure => { + expect(failure.tag).toBe('v1'); + expect(failure.value).toEqual(error); + }, + ); + }); + + it('should reject cancel when Notifications permission is denied', async () => { + const { container, hostApi } = setup(); + + container.handleDevicePermission((_, { ok }) => ok(false)); + const handler = vi.fn>((_, { ok }) => + ok(undefined), + ); + container.handlePushNotificationCancel(handler); + + const result = await hostApi.pushNotificationCancel(enumValue('v1', 5)); + + expect(result.isErr()).toBe(true); + expect(handler).not.toHaveBeenCalled(); + }); +}); diff --git a/docs/design/host-api-protocol.md b/docs/design/host-api-protocol.md index 343e011a..98803dc0 100644 --- a/docs/design/host-api-protocol.md +++ b/docs/design/host-api-protocol.md @@ -9,6 +9,13 @@ created: 2026-03-13 ## Changelog +### v0.8 - 2026-05-14 + +- **Breaking change.** Extended `host_push_notification`: request gains `scheduled_at: Option` (Unix ms UTC) for deferred delivery; response changed from `Result<(), GenericErr>` to `Result` (RFC-0019). The prior wire format is replaced rather than versioned alongside. +- Added `host_push_notification_cancel(NotificationId) -> Result<(), GenericErr>` for retracting a pending scheduled notification. +- Both methods are gated by `DevicePermission::Notifications`. No new permission variant. +- `NotificationId` is opaque per-product; cancellation is idempotent. Persistence, OS-scheduler integration, and the platform-wide queue cap are host-application concerns, not SDK concerns. + ### v0.7 - 2026-04-13 - Renamed all `*_with_non_product_account` methods to `*_with_legacy_account`; renamed `host_get_non_product_accounts` to `host_get_legacy_accounts`; updated glossary term "Non-product account (NPA)" to "Legacy account". @@ -87,7 +94,11 @@ fn host_feature_supported( ) -> Result; fn host_push_notification( - text: str + notification: PushNotification +) -> Result; + +fn host_push_notification_cancel( + identifier: NotificationId ) -> Result<(), GenericErr>; fn host_navigate_to( @@ -136,13 +147,11 @@ fn host_account_create_proof( fn host_get_legacy_accounts() -> Result, RequestCredentialsErr>; fn host_create_transaction( - accountId: ProductAccountId, - payload: VersionedTxPayload + payload: TxPayloadV1 ) -> Result, CreateTransactionErr>; fn host_create_transaction_with_legacy_account( - accountId: AccountId, - payload: VersionedTxPayload + payload: TxPayloadV1 ) -> Result, CreateTransactionErr>; fn host_sign_raw_with_legacy_account( @@ -484,6 +493,41 @@ fn host_derive_entropy( ) -> Result; ``` +#### Push Notifications + +Products can request the host to display a notification, either immediately or scheduled for a future wall-clock instant. Both methods are gated by `DevicePermission::Notifications`. See [RFC-0019](https://github.com/paritytech/truapi/blob/main/docs/rfcs/0019-scheduled-notifications.md) for the full motivation and host-side behavioural requirements (persistence, OS-scheduler integration, platform-wide queue cap). + +```rust +type NotificationId = u32; + +struct PushNotification { + // Notification body text. + text: String, + // Optional URL to open when the user taps the notification. + deeplink: Option, + // Optional Unix timestamp in milliseconds (UTC) at which the notification should fire. + // `None` fires immediately, preserving prior behaviour. Past timestamps fire immediately. + scheduled_at: Option, +} + +enum PushNotificationError { + // The host has reached its platform-wide cap on pending scheduled notifications. + // Only returned for scheduled (non-immediate) calls. + ScheduleLimitReached, + Unknown(GenericErr) +} + +fn host_push_notification( + notification: PushNotification +) -> Result; + +fn host_push_notification_cancel( + identifier: NotificationId +) -> Result<(), GenericErr>; +``` + +`NotificationId` is opaque and unique per product; the host MUST NOT leak ids across products. An id is returned for every call — immediate or scheduled — for shape uniformity. Cancellation is idempotent: `host_push_notification_cancel` MUST return `Ok(())` regardless of whether the id refers to a pending, already-fired, never-issued, or other-product's notification. This is a breaking change relative to the pre-v0.8 wire format; products and hosts must upgrade together. + #### Device permissions request Products can request additional device permissions. This check is layered on top of platform permissions (web, iOS, Android) and adds a product-level security gate. @@ -719,14 +763,17 @@ fn host_get_legacy_accounts() -> Result, RequestCredentialsEr #### Create Transaction -Based on [https://github.com/polkadot-js/api/issues/6213](https://github.com/polkadot-js/api/issues/6213), but omitting the `version` field.\ -This format is capable of supporting both V4 and V5 extrinsics. -There are two different methods for creating a transaction: `create_transaction` and `create_transaction_with_legacy_account`. `create_transaction` is bound to the Host API account model; `create_transaction_with_legacy_account`, on the other hand, can request signing with any legacy account, and the host should decide how to find or derive accounts for signing using the `signer` field as a reference. +Derived from [https://github.com/polkadot-js/api/issues/6213](https://github.com/polkadot-js/api/issues/6213) but trimmed for an online-signer topology: the `version` field is omitted, and the `context` block (runtime metadata, token symbol/decimals, best block height) is dropped — the signer (Host or Account Holder) is online and derives those from the chain identified by `CheckGenesis` rather than trusting a product-supplied blob. + +The format is capable of supporting both V4 and V5 extrinsics. + +There are two methods for creating a transaction: `create_transaction` and `create_transaction_with_legacy_account`. Both take a `TxPayloadV1` parametrized by the signer-identifier type — `ProductAccountId` for product accounts and `AccountId` for legacy accounts. The `signer` field is required and typed; there is no separate `account_id` parameter. ```rust enum CreateTransactionErr { FailedToDecode, Rejected, + // Unsupported payload version // Failed to infer missing extensions, some extension is unsupported, etc. NotSupported(str), PermissionDenied, @@ -739,35 +786,24 @@ struct TxPayloadExtensionV1 { additional_signed: Vec } -struct TxPayloadContext { - metadata: Vec, - token_symbol: str, - token_decimals: u32, - best_block_height: u32 -} - -struct TxPayloadV1 { - signer: Option, +struct TxPayloadV1 { + signer: Signer, call_data: Vec, extensions: Vec, - tx_ext_version: u8, - context: TxPayloadContext -} - -enum VersionedTxPayload { - V1(TxPayloadV1) + tx_ext_version: u8 } fn host_create_transaction( - account_id: ProductAccountId, - payload: VersionedTxPayload + payload: TxPayloadV1 ) -> Result, CreateTransactionErr>; fn host_create_transaction_with_legacy_account( - payload: VersionedTxPayload + payload: TxPayloadV1 ) -> Result, CreateTransactionErr>; ``` +> Note: `TxPayloadV1` is type-level shorthand for one concrete encoding per call site; the SCALE codec on the wire is not generic. + #### Signing Raw Signing of raw bytes. The interface implementation is similar to `signRaw` from `injectedWeb3`, added for backward compatibility. diff --git a/package-lock.json b/package-lock.json index a123f2bb..9cc5d634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1089,9 +1089,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2817,9 +2817,9 @@ "link": true }, "node_modules/@novasamatech/tr-ui": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@novasamatech/tr-ui/-/tr-ui-0.2.7.tgz", - "integrity": "sha512-awZLLQLKH/cpz8OCfV/XwEvHDxVtmL27cLv/NglOd98eDkf+tk/yJAktAc07g42ZlmVYQOzPv+Phl/s6nAyDXw==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@novasamatech/tr-ui/-/tr-ui-0.2.9.tgz", + "integrity": "sha512-HwExQ1Y6lh5fIdbxtjm8XspfiUaISBEHYsw8X+bVM7z2CZOfDJy+AlMw5QODU6/qpvpleYydFs/K2cV78pwCaA==", "license": "Apache-2.0", "dependencies": { "@fontsource/inter": "^5", @@ -8300,6 +8300,16 @@ "postcss": "^8.0.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -9253,7 +9263,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9263,7 +9272,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -9685,6 +9693,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -9889,7 +9906,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -9902,7 +9918,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colord": { @@ -10152,6 +10167,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -10393,6 +10417,12 @@ "node": ">=0.3.1" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10518,7 +10548,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/empathic": { @@ -11286,9 +11315,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -11600,7 +11629,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -12174,7 +12202,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13612,6 +13639,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13684,7 +13720,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13818,6 +13853,15 @@ "pathe": "^2.0.3" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/pnglib": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pnglib/-/pnglib-0.0.1.tgz", @@ -14146,23 +14190,140 @@ "node": ">=6" } }, - "node_modules/qr-code-styling": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", - "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { - "qrcode-generator": "^1.4.4" + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" }, "engines": { - "node": ">=18.18.0" + "node": ">=10.13.0" } }, - "node_modules/qrcode-generator": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz", - "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==", - "license": "MIT" + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } }, "node_modules/quansync": { "version": "0.2.11", @@ -14579,7 +14740,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14595,6 +14755,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/reserved-words": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", @@ -14915,6 +15081,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15215,7 +15387,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15230,7 +15401,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -16179,12 +16349,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/verifiablejs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/verifiablejs/-/verifiablejs-1.2.0.tgz", - "integrity": "sha512-+VVQo9yZko+w3THKaYvLbjXdfaiw1WJnX12wJEU5jHsHrMkjDrAMWoKYcBvtlHXufJirr8FlT6v2i/0yA3/ivQ==", - "license": "GPL-3.0-or-later WITH Classpath-exception-2.0" - }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", @@ -16502,6 +16666,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -16881,7 +17051,7 @@ }, "packages/handoff-service": { "name": "@novasamatech/handoff-service", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "2.2.0", @@ -16894,10 +17064,10 @@ }, "packages/host-api": { "name": "@novasamatech/host-api", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/scale": "0.7.8", + "@novasamatech/scale": "0.7.9", "nanoevents": "9.1.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0", @@ -16906,16 +17076,29 @@ }, "packages/host-api-wrapper": { "name": "@novasamatech/host-api-wrapper", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/host-api": "0.7.8", + "@novasamatech/host-api": "0.7.9", "@polkadot-api/json-rpc-provider-proxy": "^0.4.0", + "@polkadot-api/substrate-bindings": "^0.20.2", "@polkadot/extension-inject": "^0.63.1", "neverthrow": "^8.2.0", "polkadot-api": ">=2" } }, + "packages/host-api-wrapper/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.2.tgz", + "integrity": "sha512-js5UTREoI+FlrPRXMhtKimVWmOqwfNFBnhyshsdloSZHNx/Hulg2RQZNvrVTscyZTf8LyxlGJaH5dsitOUoFKw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, "packages/host-api/node_modules/nanoid": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", @@ -16936,12 +17119,12 @@ }, "packages/host-chat": { "name": "@novasamatech/host-chat", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/scale": "0.7.8", - "@novasamatech/statement-store": "0.7.8", - "@novasamatech/storage-adapter": "0.7.8", + "@novasamatech/scale": "0.7.9", + "@novasamatech/statement-store": "0.7.9", + "@novasamatech/storage-adapter": "0.7.9", "nanoid": "5.1.9", "neverthrow": "^8.2.0" } @@ -16966,11 +17149,11 @@ }, "packages/host-container": { "name": "@novasamatech/host-container", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { "@noble/hashes": "2.2.0", - "@novasamatech/host-api": "0.7.8", + "@novasamatech/host-api": "0.7.9", "@polkadot-api/substrate-client": "^0.7.0", "nanoevents": "9.1.0", "nanoid": "5.1.9", @@ -17001,40 +17184,40 @@ }, "packages/host-papp": { "name": "@novasamatech/host-papp", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "2.2.0", "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", - "@novasamatech/host-api": "0.7.8", - "@novasamatech/scale": "0.7.8", - "@novasamatech/statement-store": "0.7.8", - "@novasamatech/storage-adapter": "0.7.8", + "@novasamatech/host-api": "0.7.9", + "@novasamatech/scale": "0.7.9", + "@novasamatech/statement-store": "0.7.9", + "@novasamatech/storage-adapter": "0.7.9", "@polkadot-labs/hdkd-helpers": "^0.0.30", "nanoevents": "9.1.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0", "polkadot-api": ">=2", - "scale-ts": "1.6.1", - "verifiablejs": "1.2.0" + "scale-ts": "1.6.1" } }, "packages/host-papp-react-ui": { "name": "@novasamatech/host-papp-react-ui", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/host-papp": "0.7.8", - "@novasamatech/statement-store": "0.7.8", - "@novasamatech/tr-ui": "0.2.7", + "@novasamatech/host-papp": "0.7.9", + "@novasamatech/statement-store": "0.7.9", + "@novasamatech/tr-ui": "0.2.9", "@polkadot-api/utils": "^0.4.0", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-popover": "1.1.15", - "qr-code-styling": "1.9.2" + "qrcode": "^1.5.4" }, "devDependencies": { - "@polkadot-api/substrate-bindings": "^0.20.0", + "@polkadot-api/substrate-bindings": "^0.20.2", + "@types/qrcode": "^1.5.6", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "react": "19.2.5", @@ -17047,6 +17230,19 @@ "react-dom": ">=18" } }, + "packages/host-papp-react-ui/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.2.tgz", + "integrity": "sha512-js5UTREoI+FlrPRXMhtKimVWmOqwfNFBnhyshsdloSZHNx/Hulg2RQZNvrVTscyZTf8LyxlGJaH5dsitOUoFKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, "packages/host-papp/node_modules/nanoid": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", @@ -17067,10 +17263,10 @@ }, "packages/host-substrate-chain-connection": { "name": "@novasamatech/host-substrate-chain-connection", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/storage-adapter": "0.7.8", + "@novasamatech/storage-adapter": "0.7.9", "@polkadot-api/json-rpc-provider": "^0.2.0", "@polkadot-api/json-rpc-provider-proxy": "^0.4.0", "@polkadot-api/ws-provider": "^0.9.0", @@ -17080,31 +17276,31 @@ }, "packages/host-worker-sandbox": { "name": "@novasamatech/host-worker-sandbox", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/host-api": "0.7.8", - "@novasamatech/host-container": "0.7.8", + "@novasamatech/host-api": "0.7.9", + "@novasamatech/host-container": "0.7.9", "quickjs-emscripten": "0.32.0" } }, "packages/product-bulletin": { "name": "@novasamatech/product-bulletin", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/host-api-wrapper": "0.7.8", + "@novasamatech/host-api-wrapper": "0.7.9", "@parity/bulletin-sdk": "^0.3.0", "polkadot-api": ">=2" } }, "packages/product-react-renderer": { "name": "@novasamatech/product-react-renderer", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { - "@novasamatech/host-api": "0.7.8", - "@novasamatech/host-api-wrapper": "0.7.8", + "@novasamatech/host-api": "0.7.9", + "@novasamatech/host-api-wrapper": "0.7.9", "react-reconciler": "0.33.0", "scale-ts": "1.6.1" }, @@ -17117,12 +17313,13 @@ }, "packages/product-sdk": { "name": "@novasamatech/product-sdk", - "version": "0.7.8", + "version": "0.7.9-4", "extraneous": true, "license": "Apache-2.0", "dependencies": { - "@novasamatech/host-api": "0.7.8", + "@novasamatech/host-api": "0.7.9-4", "@polkadot-api/json-rpc-provider-proxy": "^0.4.0", + "@polkadot-api/substrate-bindings": "^0.20.2", "@polkadot/extension-inject": "^0.63.1", "neverthrow": "^8.2.0", "polkadot-api": ">=2" @@ -17130,7 +17327,7 @@ }, "packages/scale": { "name": "@novasamatech/scale", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { "@polkadot-api/utils": "^0.4.0", @@ -17139,14 +17336,14 @@ }, "packages/statement-store": { "name": "@novasamatech/statement-store", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "2.2.0", "@noble/hashes": "2.2.0", - "@novasamatech/scale": "0.7.8", + "@novasamatech/scale": "0.7.9", "@novasamatech/sdk-statement": "^0.6.0", - "@polkadot-api/substrate-bindings": "^0.20.1", + "@polkadot-api/substrate-bindings": "^0.20.2", "@polkadot-api/substrate-client": "^0.7.0", "@polkadot-labs/hdkd-helpers": "^0.0.30", "@scure/sr25519": "2.2.0", @@ -17156,6 +17353,18 @@ "scale-ts": "1.6.1" } }, + "packages/statement-store/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.2.tgz", + "integrity": "sha512-js5UTREoI+FlrPRXMhtKimVWmOqwfNFBnhyshsdloSZHNx/Hulg2RQZNvrVTscyZTf8LyxlGJaH5dsitOUoFKw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@polkadot-api/utils": "0.4.0", + "@scure/base": "^2.2.0", + "scale-ts": "^1.6.1" + } + }, "packages/statement-store/node_modules/nanoid": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", @@ -17176,7 +17385,7 @@ }, "packages/storage-adapter": { "name": "@novasamatech/storage-adapter", - "version": "0.7.8", + "version": "0.7.9", "license": "Apache-2.0", "dependencies": { "nanoevents": "^9.1.0", diff --git a/packages/handoff-service/package.json b/packages/handoff-service/package.json index 6b2533af..ea57742f 100644 --- a/packages/handoff-service/package.json +++ b/packages/handoff-service/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/handoff-service", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "HOP (Handoff Pool) file transfer service for P2P chat", "license": "Apache-2.0", "repository": { diff --git a/packages/host-api-wrapper/README.md b/packages/host-api-wrapper/README.md index 6a1e2478..b286c194 100644 --- a/packages/host-api-wrapper/README.md +++ b/packages/host-api-wrapper/README.md @@ -211,15 +211,12 @@ subscription.unsubscribe(); The Accounts Provider allows you to access product accounts and create signers for signing transactions. ```ts -import { createAccountsProvider } from '@novasamatech/host-api-wrapper'; +import { accounts } from '@novasamatech/host-api-wrapper'; import type { ProductAccount } from '@novasamatech/host-api-wrapper'; -// Create accounts provider instance -const accountsProvider = createAccountsProvider(); - // Get the user's primary DotNS username (RFC-0014) // — prompts for permission on first call -const userIdResult = await accountsProvider.getUserId(); +const userIdResult = await accounts.getUserId(); if (userIdResult.isOk()) { const { primaryUsername } = userIdResult.value; @@ -234,7 +231,7 @@ if (userIdResult.isOk()) { } // Request login — triggers host sign-in UI; reason is shown to the user -const loginResult = await accountsProvider.requestLogin('Sign in to access your account'); +const loginResult = await accounts.requestLogin('Sign in to access your account'); if (loginResult.isOk()) { const outcome = loginResult.value; // 'success' | 'alreadyConnected' | 'rejected' @@ -246,7 +243,7 @@ if (loginResult.isOk()) { } // Get a product account by DotNS identifier and derivation index -const accountResult = await accountsProvider.getProductAccount('product.dot', 0); +const accountResult = await accounts.getProductAccount('product.dot', 0); if (accountResult.isOk()) { const account: ProductAccount = accountResult.value; @@ -254,42 +251,49 @@ if (accountResult.isOk()) { } // Get account alias -const aliasResult = await accountsProvider.getProductAccountAlias('product.dot', 0); +const aliasResult = await accounts.getProductAccountAlias('product.dot', 0); if (aliasResult.isOk()) { console.log('Alias:', aliasResult.value); } // Get legacy accounts (external wallets) -const legacyAccountsResult = await accountsProvider.getLegacyAccounts(); +const legacyAccountsResult = await accounts.getLegacyAccounts(); if (legacyAccountsResult.isOk()) { console.log('Legacy accounts:', legacyAccountsResult.value); } // Subscribe to account connection status changes -const unsubscribe = accountsProvider.subscribeAccountConnectionStatus((status) => { +const unsubscribe = accounts.subscribeAccountConnectionStatus((status) => { // status: 'connected' | 'disconnected' console.log('Account connection status:', status); }); -// Create a signer for a product account (for use with PAPI) -const account: ProductAccount = { - dotNsIdentifier: 'product.dot', - derivationIndex: 0, - publicKey: new Uint8Array([/* ... */]) -}; -const signer = accountsProvider.getProductAccountSigner(account); +// Create a signer for a product account (for use with PAPI). +// Resolve the account first, then hand it to the signer factory. +const productAccountResult = await accounts.getProductAccount('product.dot', 0); -// Create a signer for a legacy account -const legacySigner = accountsProvider.getLegacyAccountSigner(account); +if (productAccountResult.isOk()) { + const productSigner = accounts.getProductAccountSigner(productAccountResult.value); + const signedTx = await tx.signAndSubmit(productSigner); +} -// PAPI transaction signing example +// Create a signer for a legacy account. +// Fetch the legacy account list, pick one, then pass it to the signer factory. +const legacyAccountsResult = await accounts.getLegacyAccounts(); -const productAccountSignedTx = await tx.signAndSubmit(signer); -const legacyAccountSignedTx = await tx.signAndSubmit(legacySigner); +if (legacyAccountsResult.isOk()) { + const [legacyAccount] = legacyAccountsResult.value; + if (legacyAccount) { + const legacySigner = accounts.getLegacyAccountSigner(legacyAccount); + const signedTx = await tx.signAndSubmit(legacySigner); + } +} ``` +> If you need a non-default transport (e.g. for tests or multi-host setups), use `createAccountsProvider(transport)` to build your own instance with the same API. + ### Local Storage The Local Storage module provides a way to persist data in the host application's storage. diff --git a/packages/host-api-wrapper/package.json b/packages/host-api-wrapper/package.json index 7275352e..6fcee19d 100644 --- a/packages/host-api-wrapper/package.json +++ b/packages/host-api-wrapper/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-api-wrapper", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Host API wrapper: integrate and run your product inside Polkadot browser.", "license": "Apache-2.0", "repository": { @@ -27,7 +27,8 @@ "dependencies": { "@polkadot/extension-inject": "^0.63.1", "@polkadot-api/json-rpc-provider-proxy": "^0.4.0", - "@novasamatech/host-api": "0.7.8", + "@polkadot-api/substrate-bindings": "^0.20.2", + "@novasamatech/host-api": "0.7.9", "polkadot-api": ">=2", "neverthrow": "^8.2.0" }, diff --git a/packages/host-api-wrapper/src/accounts.ts b/packages/host-api-wrapper/src/accounts.ts index 7eb845e3..a0ed9e01 100644 --- a/packages/host-api-wrapper/src/accounts.ts +++ b/packages/host-api-wrapper/src/accounts.ts @@ -2,6 +2,9 @@ import type { AccountConnectionStatus as AccountConnectionStatusCodec, CodecType, HexString, + LegacyAccount as LegacyAccountCodec, + ProductAccountId as ProductAccountIdCodec, + ProductAccountTransaction, Subscription, Transport, } from '@novasamatech/host-api'; @@ -22,18 +25,23 @@ import { isEnumVariant, toHex, } from '@novasamatech/host-api'; +import { decAnyMetadata, unifyMetadata } from '@polkadot-api/substrate-bindings'; import { err, ok } from 'neverthrow'; import type { PolkadotSigner } from 'polkadot-api'; import { getPolkadotSignerFromPjs } from 'polkadot-api/pjs-signer'; import { sandboxTransport } from './sandboxTransport.js'; +export type ProductAccountId = CodecType; + export type ProductAccount = { dotNsIdentifier: string; derivationIndex: number; publicKey: Uint8Array; }; +export type LegacyAccount = CodecType; + export type AccountConnectionStatus = CodecType; const UNSUPPORTED_VERSION_ERROR = 'Unsupported message version'; @@ -72,7 +80,11 @@ export const createAccountsProvider = (transport: Transport = sandboxTransport) .mapErr(e => e.value) .andThen(response => { if (isEnumVariant(response, 'v1')) { - return ok(response.value); + return ok({ + publicKey: response.value.publicKey, + dotNsIdentifier, + derivationIndex, + } satisfies ProductAccount); } // @ts-expect-error response.tag is never here return err(new RequestCredentialsErr.Unknown({ reason: `Unsupported response version ${response.tag}` })); @@ -119,25 +131,116 @@ export const createAccountsProvider = (transport: Transport = sandboxTransport) return err(new CreateProofErr.Unknown({ reason: `Unsupported response version ${response.tag}` })); }); }, - getProductAccountSigner(account: ProductAccount): PolkadotSigner { - return getPolkadotSignerFromPjs( - toHex(account.publicKey), - async payload => { - const codecPayload: CodecType = { - account: [account.dotNsIdentifier, account.derivationIndex], - payload: buildSigningPayloadFields(payload), + + /** + * Builds a `PolkadotSigner` that delegates to the host via `host_create_transaction`. + * + * The factory is async because `PolkadotSigner.publicKey` must be a synchronous + * `Uint8Array` on the returned object — it is fetched up front via `host_account_get`. + */ + getProductAccountSigner( + account: ProductAccount, + signerType: 'signPayload' | 'createTransaction' = 'createTransaction', + ): PolkadotSigner { + const hostApi = createHostApi(transport); + const productAccountId: ProductAccountId = [account.dotNsIdentifier, account.derivationIndex]; + + /** + * @deprecated added for backward compatibility + */ + if (signerType === 'signPayload') { + return getPolkadotSignerFromPjs( + toHex(account.publicKey), + async payload => { + const codecPayload: CodecType = { + account: [account.dotNsIdentifier, account.derivationIndex], + payload: buildSigningPayloadFields(payload), + }; + + const response = await hostApi.signPayload(enumValue('v1', codecPayload)); + + return response.match( + response => { + assertEnumVariant(response, 'v1', UNSUPPORTED_VERSION_ERROR); + return { + id: 0, + signature: response.value.signature, + signedTransaction: response.value.signedTransaction, + }; + }, + err => { + assertEnumVariant(err, 'v1', UNSUPPORTED_VERSION_ERROR); + throw err.value; + }, + ); + }, + async raw => { + const payload: CodecType = { + account: [account.dotNsIdentifier, account.derivationIndex], + payload: + raw.type === 'bytes' + ? { + tag: 'Bytes', + value: fromHex(asHex(raw.data)), + } + : { + tag: 'Payload', + value: raw.data, + }, + }; + + const response = await hostApi.signRaw(enumValue('v1', payload)); + + return response.match( + response => { + assertEnumVariant(response, 'v1', UNSUPPORTED_VERSION_ERROR); + return { + id: 0, + signature: response.value.signature, + signedTransaction: response.value.signedTransaction, + }; + }, + err => { + assertEnumVariant(err, 'v1', UNSUPPORTED_VERSION_ERROR); + throw err.value; + }, + ); + }, + ); + } + + return { + publicKey: account.publicKey, + + async signTx(callData, signedExtensions, metadata) { + const decMeta = unifyMetadata(decAnyMetadata(metadata)); + const { version: versions } = decMeta.extrinsic; + const latestVersion = versions.reduce((acc, v) => Math.max(acc, v), 0); + const txExtVersion = latestVersion === 4 ? 0 : latestVersion; + + const checkGenesis = signedExtensions['CheckGenesis']; + if (!checkGenesis) { + throw new Error("Can't find genesis hash on transaction"); + } + + const txPayload: CodecType = { + signer: productAccountId, + genesisHash: checkGenesis.additionalSigned, + callData, + extensions: Object.values(signedExtensions).map(({ identifier, value, additionalSigned }) => ({ + id: identifier, + extra: value, + additionalSigned: additionalSigned, + })), + txExtVersion, }; - const response = await hostApi.signPayload(enumValue('v1', codecPayload)); + const response = await hostApi.createTransaction(enumValue('v1', txPayload)); return response.match( response => { assertEnumVariant(response, 'v1', UNSUPPORTED_VERSION_ERROR); - return { - id: 0, - signature: response.value.signature, - signedTransaction: response.value.signedTransaction, - }; + return response.value; }, err => { assertEnumVariant(err, 'v1', UNSUPPORTED_VERSION_ERROR); @@ -145,31 +248,19 @@ export const createAccountsProvider = (transport: Transport = sandboxTransport) }, ); }, - async raw => { - const payload: CodecType = { - account: [account.dotNsIdentifier, account.derivationIndex], - payload: - raw.type === 'bytes' - ? { - tag: 'Bytes', - value: fromHex(asHex(raw.data)), - } - : { - tag: 'Payload', - value: raw.data, - }, - }; - const response = await hostApi.signRaw(enumValue('v1', payload)); + async signBytes(data) { + const response = await hostApi.signRaw( + enumValue('v1', { + account: productAccountId, + payload: { tag: 'Bytes', value: data }, + }), + ); return response.match( response => { assertEnumVariant(response, 'v1', UNSUPPORTED_VERSION_ERROR); - return { - id: 0, - signature: response.value.signature, - signedTransaction: response.value.signedTransaction, - }; + return fromHex(response.value.signature); }, err => { assertEnumVariant(err, 'v1', UNSUPPORTED_VERSION_ERROR); @@ -177,7 +268,7 @@ export const createAccountsProvider = (transport: Transport = sandboxTransport) }, ); }, - ); + }; }, subscribeAccountConnectionStatus(callback: (status: AccountConnectionStatus) => void): Subscription { const subscriber = hostApi.accountConnectionStatusSubscribe(enumValue('v1', undefined), status => { @@ -191,7 +282,7 @@ export const createAccountsProvider = (transport: Transport = sandboxTransport) onInterrupt: cb => subscriber.onInterrupt(v => cb(v.value)), }; }, - getLegacyAccountSigner(account: ProductAccount): PolkadotSigner { + getLegacyAccountSigner(account: LegacyAccount): PolkadotSigner { return getPolkadotSignerFromPjs( toHex(account.publicKey), async payload => { @@ -245,6 +336,8 @@ export const createAccountsProvider = (transport: Transport = sandboxTransport) }; }; +export const accounts = createAccountsProvider(); + function asHex(v: string): HexString { if (v.startsWith('0x')) return v as HexString; return `0x${v}`; diff --git a/packages/host-api-wrapper/src/index.ts b/packages/host-api-wrapper/src/index.ts index 8e217684..63be7ace 100644 --- a/packages/host-api-wrapper/src/index.ts +++ b/packages/host-api-wrapper/src/index.ts @@ -30,14 +30,17 @@ export type { } from './statementStore.js'; export { createStatementStore } from './statementStore.js'; -export type { AccountConnectionStatus, ProductAccount } from './accounts.js'; -export { createAccountsProvider } from './accounts.js'; +export type { AccountConnectionStatus, LegacyAccount, ProductAccount } from './accounts.js'; +export { accounts, createAccountsProvider } from './accounts.js'; export type { ThemeMode } from './theme.js'; export { createThemeProvider } from './theme.js'; export { createLocalStorage, hostLocalStorage } from './localStorage.js'; +export type { NotificationId, PushNotificationInput } from './notification.js'; +export { createNotificationManager, notificationManager } from './notification.js'; + export { createPreimageManager, preimageManager } from './preimage.js'; export type { PaymentBalance, PaymentStatus, TopUpSource } from './payments.js'; diff --git a/packages/host-api-wrapper/src/injectWeb3.ts b/packages/host-api-wrapper/src/injectWeb3.ts index afb31c3b..60d57c60 100644 --- a/packages/host-api-wrapper/src/injectWeb3.ts +++ b/packages/host-api-wrapper/src/injectWeb3.ts @@ -1,15 +1,92 @@ -import type { CodecType, HexString, Transport, VersionedPublicTxPayload } from '@novasamatech/host-api'; +import type { HexString, Transport } from '@novasamatech/host-api'; import { assertEnumVariant, createHostApi, enumValue, fromHex, toHex } from '@novasamatech/host-api'; import { injectExtension } from '@polkadot/extension-inject'; import type { InjectedAccount, InjectedAccounts } from '@polkadot/extension-inject/types'; import type { SignerPayloadJSON, SignerPayloadRaw, SignerResult } from '@polkadot/types/types/extrinsic'; import { AccountId } from 'polkadot-api'; +import { createAccountsProvider } from './accounts.js'; import { SpektrExtensionName, Version } from './constants.js'; import { sandboxTransport } from './sandboxTransport.js'; const UNSUPPORTED_VERSION_ERROR = 'Unsupported message version'; +/** + * expected interface derived from specification + */ +export interface TxPayloadV1 { + /** Payload version. MUST be 1. */ + version: 1; + + /** + * Signer selection hint. Allows the implementer to identify which private-key / scheme to use. + * - Use a wallet-defined handle (e.g., address/SS58, account-name, etc). This identifier + * was previously made available to the consumer. + * - Set `null` to let the implementer pick the signer (or if the signer is implied). + */ + signer: string | null; + + /** + * SCALE-encoded Call (module indicator + function indicator + params). + */ + callData: HexString; + + /** + * Transaction extensions supplied by the caller (order irrelevant). + * The consumer SHOULD provide every extension that is relevant to them. + * The implementer MAY infer missing ones. + */ + extensions: Array<{ + /** Identifier as defined in metadata (e.g., "CheckSpecVersion", "ChargeAssetTxPayment"). */ + id: string; + + /** + * Explicit "extra" to sign (goes into the extrinsic body). + * SCALE-encoded per the extension's "extra" type as defined in the metadata. + */ + extra: HexString; + + /** + * "Implicit" data to sign (known by the chain, not included into the extrinsic body). + * SCALE-encoded per the extension's "additionalSigned" type as defined in the metadata. + */ + additionalSigned: HexString; + }>; + + /** + * Transaction Extension Version. + * - For Extrinsic V4 MUST be 0. + * - For Extrinsic V5, set to any version supported by the runtime. + * The implementer: + * - MUST use this field to determine the required extensions for creating the extrinsic. + * - MAY use this field to infer missing extensions that the implementer could know how to handle. + */ + txExtVersion: number; + + /** + * Context needed for decoding, display, and (optionally) inferring certain extensions. + */ + context: { + /** + * RuntimeMetadataPrefixed blob (SCALE), starting with ASCII "meta" magic (`0x6d657461`), + * then a metadata version (V14+). For V5+ versioned extensions, MUST provide V16+. + */ + metadata: HexString; + + /** + * Native token display info (used by some implementers), also needed to compute + * the `CheckMetadataHash` value. + */ + tokenSymbol: string; + tokenDecimals: number; + + /** + * Highest known block number to aid mortality UX. + */ + bestBlockHeight: number; + }; +} + interface Signer { /** * @description signs an extrinsic payload from a serialized form @@ -22,7 +99,7 @@ interface Signer { /** * @description signs a transaction according to https://github.com/polkadot-js/api/issues/6213 */ - createTransaction?: (payload: CodecType) => Promise; + createTransaction?: (payload: TxPayloadV1) => Promise; } interface Injected { @@ -34,28 +111,27 @@ export async function createLegacyExtensionEnableFactory(transport: Transport) { const ready = await transport.isReady(); if (!ready) return null; + const accountsManager = createAccountsProvider(transport); const hostApi = createHostApi(transport); const accountId = AccountId(); async function enable(): Promise { async function getAccounts() { - const response = await hostApi.getLegacyAccounts(enumValue('v1', undefined)); - - return response.match( - response => { - assertEnumVariant(response, 'v1', UNSUPPORTED_VERSION_ERROR); - - return response.value.map(account => ({ + return await accountsManager + .getLegacyAccounts() + .map(response => { + return response.map(account => ({ name: account.name, address: accountId.dec(account.publicKey), type: 'sr25519', })); - }, - err => { - assertEnumVariant(err, 'v1', UNSUPPORTED_VERSION_ERROR); - throw err.value; - }, - ); + }) + .match( + x => x, + x => { + throw x; + }, + ); } return { @@ -144,7 +220,31 @@ export async function createLegacyExtensionEnableFactory(transport: Transport) { ); }, async createTransaction(payload) { - const response = await hostApi.createTransactionWithLegacyAccount(enumValue('v1', payload)); + if (payload.version !== 1) { + throw new Error(`Signer support only v1 transaction, got version = ${payload.version}`); + } + const { signer } = payload; + if (!signer) { + throw new Error("Signer can't route transaction to the right account without signer hint."); + } + const checkGenesis = payload.extensions.find(x => x.id === 'CheckGenesis'); + if (!checkGenesis) { + throw new Error("Can't find genesis hash on transaction"); + } + const possibleAccountId = accountId.enc(signer); + const response = await hostApi.createTransactionWithLegacyAccount( + enumValue('v1', { + signer: possibleAccountId, + genesisHash: fromHex(checkGenesis.additionalSigned), + callData: fromHex(payload.callData), + txExtVersion: payload.txExtVersion, + extensions: payload.extensions.map(e => ({ + id: e.id, + additionalSigned: fromHex(e.additionalSigned), + extra: fromHex(e.extra), + })), + }), + ); return response.match( response => { diff --git a/packages/host-api-wrapper/src/notification.ts b/packages/host-api-wrapper/src/notification.ts new file mode 100644 index 00000000..b4de40d9 --- /dev/null +++ b/packages/host-api-wrapper/src/notification.ts @@ -0,0 +1,42 @@ +import { createHostApi, enumValue } from '@novasamatech/host-api'; + +import { resultToPromise, unwrapVersionedResult } from './helpers.js'; +import { sandboxTransport } from './sandboxTransport.js'; + +export type NotificationId = number; + +export type PushNotificationInput = { + text: string; + deeplink?: string; + /** Unix timestamp in milliseconds (UTC). Omit for immediate delivery. Past values fire immediately. */ + scheduledAt?: number; +}; + +export const createNotificationManager = (transport = sandboxTransport) => { + const supportedVersion = 'v1'; + const hostApi = createHostApi(transport); + + return { + push({ text, deeplink, scheduledAt }: PushNotificationInput): Promise { + return resultToPromise( + unwrapVersionedResult( + supportedVersion, + hostApi.pushNotification( + enumValue(supportedVersion, { + text, + deeplink, + scheduledAt: scheduledAt === undefined ? undefined : BigInt(scheduledAt), + }), + ), + ), + ); + }, + cancel(id: NotificationId): Promise { + return resultToPromise( + unwrapVersionedResult(supportedVersion, hostApi.pushNotificationCancel(enumValue(supportedVersion, id))), + ); + }, + }; +}; + +export const notificationManager = createNotificationManager(); diff --git a/packages/host-api/package.json b/packages/host-api/package.json index 53418e2b..547ee9b6 100644 --- a/packages/host-api/package.json +++ b/packages/host-api/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-api", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Host API: transport implementation for host - product integration.", "license": "Apache-2.0", "repository": { @@ -22,7 +22,7 @@ "README.md" ], "dependencies": { - "@novasamatech/scale": "0.7.8", + "@novasamatech/scale": "0.7.9", "nanoevents": "9.1.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0", diff --git a/packages/host-api/src/hostApi.ts b/packages/host-api/src/hostApi.ts index cdc38add..6c9dee3a 100644 --- a/packages/host-api/src/hostApi.ts +++ b/packages/host-api/src/hostApi.ts @@ -12,6 +12,7 @@ import { DeriveEntropyErr } from './protocol/v1/deriveEntropy.js'; import { HandshakeErr } from './protocol/v1/handshake.js'; import { StorageErr } from './protocol/v1/localStorage.js'; import { NavigateToErr } from './protocol/v1/navigation.js'; +import { PushNotificationError } from './protocol/v1/notification.js'; import { PaymentRequestErr, PaymentTopUpErr } from './protocol/v1/payments.js'; import { PreimageSubmitErr } from './protocol/v1/preimage.js'; import { ResourceAllocationErr } from './protocol/v1/resourceAllocation.js'; @@ -103,6 +104,13 @@ export function createHostApi(transport: Transport): HostApi { pushNotification(payload) { return makeRequest(transport.request('host_push_notification', payload), reason => ({ + tag: payload.tag, + value: new PushNotificationError.Unknown({ reason }), + })); + }, + + pushNotificationCancel(payload) { + return makeRequest(transport.request('host_push_notification_cancel', payload), reason => ({ tag: payload.tag, value: new GenericError({ reason }), })); diff --git a/packages/host-api/src/index.ts b/packages/host-api/src/index.ts index 360f2dcc..6df692ca 100644 --- a/packages/host-api/src/index.ts +++ b/packages/host-api/src/index.ts @@ -38,7 +38,7 @@ export { // Codecs export { GenericError } from './protocol/commonCodecs.js'; -export { CreateTransactionErr, VersionedPublicTxPayload } from './protocol/v1/createTransaction.js'; +export { CreateTransactionErr, LegacyTransaction, ProductAccountTransaction } from './protocol/v1/createTransaction.js'; export { AccountConnectionStatus, AccountId, @@ -89,7 +89,7 @@ export { export { StorageErr } from './protocol/v1/localStorage.js'; export { DevicePermission } from './protocol/v1/devicePermission.js'; export { RemotePermission } from './protocol/v1/remotePermission.js'; -export { PushNotification } from './protocol/v1/notification.js'; +export { NotificationId, PushNotification, PushNotificationError } from './protocol/v1/notification.js'; export { NavigateToErr } from './protocol/v1/navigation.js'; export { PreimageKey, PreimageSubmitErr, PreimageValue } from './protocol/v1/preimage.js'; export { AllocatableResource, AllocationOutcome, ResourceAllocationErr } from './protocol/v1/resourceAllocation.js'; diff --git a/packages/host-api/src/protocol/impl.ts b/packages/host-api/src/protocol/impl.ts index fa57d250..f3962a8d 100644 --- a/packages/host-api/src/protocol/impl.ts +++ b/packages/host-api/src/protocol/impl.ts @@ -85,7 +85,12 @@ import { StorageWriteV1_response, } from './v1/localStorage.js'; import { NavigateToV1_request, NavigateToV1_response } from './v1/navigation.js'; -import { PushNotificationV1_request, PushNotificationV1_response } from './v1/notification.js'; +import { + PushNotificationCancelV1_request, + PushNotificationCancelV1_response, + PushNotificationV1_request, + PushNotificationV1_response, +} from './v1/notification.js'; import { PaymentBalanceSubscribeV1_interrupt, PaymentBalanceSubscribeV1_receive, @@ -421,4 +426,8 @@ export const hostApiProtocol = { remote_statement_store_create_proof_authorized: versionedRequest({ v1: [StatementStoreCreateProofAuthorizedV1_request, StatementStoreCreateProofAuthorizedV1_response], }), + + host_push_notification_cancel: versionedRequest({ + v1: [PushNotificationCancelV1_request, PushNotificationCancelV1_response], + }), } as const; diff --git a/packages/host-api/src/protocol/v1/createTransaction.ts b/packages/host-api/src/protocol/v1/createTransaction.ts index 77b16a0d..97fb2d90 100644 --- a/packages/host-api/src/protocol/v1/createTransaction.ts +++ b/packages/host-api/src/protocol/v1/createTransaction.ts @@ -1,15 +1,15 @@ -import type { HexString } from '@novasamatech/scale'; -import { Enum, ErrEnum, Hex, Nullable } from '@novasamatech/scale'; -import type { CodecType } from 'scale-ts'; -import { Bytes, Result, Struct, Tuple, Vector, _void, enhanceCodec, str, u32, u8 } from 'scale-ts'; +import { ErrEnum } from '@novasamatech/scale'; +import type { Codec } from 'scale-ts'; +import { Bytes, Result, Struct, Vector, _void, str, u8 } from 'scale-ts'; import { GenericErr } from '../commonCodecs.js'; -import { ProductAccountId } from './accounts.js'; +import { AccountId, ProductAccountId } from './accounts.js'; /** * createTransaction implementation * @see https://github.com/polkadot-js/api/issues/6213 + * Since specification is aimed to cover both online and offline signers we dropped some field that are not related */ export const CreateTransactionErr = ErrEnum('CreateTransactionErr', { @@ -23,133 +23,56 @@ export const CreateTransactionErr = ErrEnum('CreateTransactionErr', { }); export const TxPayloadExtensionV1 = Struct({ + /** Identifier as defined in metadata (e.g., "CheckSpecVersion", "ChargeAssetTxPayment"). */ id: str, - extra: Hex(), - additionalSigned: Hex(), -}); - -export const TxPayloadContextV1 = Struct({ - metadata: Hex(), - tokenSymbol: str, - tokenDecimals: u32, - bestBlockHeight: u32, -}); - -export const TxPayloadV1 = Struct({ - signer: Nullable(str), - callData: Hex(), - extensions: Vector(TxPayloadExtensionV1), - txExtVersion: u8, - context: TxPayloadContextV1, -}); - -export const VersionedTxPayload = Enum({ - v1: TxPayloadV1, -}); - -export const VersionedPublicTxPayload = enhanceCodec, TxPayloadV1Public>( - VersionedTxPayload, - v => { - if (v.version !== 1) { - throw new Error(`Unsupported transaction version: ${v}`); - } - - return { - tag: 'v1', - value: v, - }; - }, - v => { - if (v.tag !== 'v1') { - throw new Error(`Unsupported transaction version: ${v}`); - } - - return { - version: 1, - ...v.value, - }; - }, -); - -// transaction in the context of a host api account model - -export const CreateTransactionV1_request = Tuple(ProductAccountId, VersionedPublicTxPayload); -export const CreateTransactionV1_response = Result(Bytes(), CreateTransactionErr); - -export const CreateTransactionWithLegacyAccountV1_request = VersionedPublicTxPayload; -export const CreateTransactionWithLegacyAccountV1_response = Result(Bytes(), CreateTransactionErr); - -// related types - -export interface TxPayloadV1Public { - /** Payload version. MUST be 1. */ - version: 1; - - /** - * Signer selection hint. Allows the implementer to identify which private-key / scheme to use. - * - Use a wallet-defined handle (e.g., address/SS58, account-name, etc). This identifier - * was previously made available to the consumer. - * - Set `null` to let the implementer pick the signer (or if the signer is implied). - */ - signer: string | null; - /** - * SCALE-encoded Call (module indicator + function indicator + params). + * Explicit "extra" to sign (goes into the extrinsic body). + * SCALE-encoded per the extension's "extra" type as defined in the metadata. */ - callData: HexString; - + extra: Bytes(), /** - * Transaction extensions supplied by the caller (order irrelevant). - * The consumer SHOULD provide every extension that is relevant to them. - * The implementer MAY infer missing ones. + * "Implicit" data to sign (known by the chain, not included into the extrinsic body). + * SCALE-encoded per the extension's "additionalSigned" type as defined in the metadata. */ - extensions: Array<{ - /** Identifier as defined in metadata (e.g., "CheckSpecVersion", "ChargeAssetTxPayment"). */ - id: string; + additionalSigned: Bytes(), +}); +function GenericTxPayloadV1(signer: Codec) { + return Struct({ + signer, /** - * Explicit "extra" to sign (goes into the extrinsic body). - * SCALE-encoded per the extension's "extra" type as defined in the metadata. + * Chain identifier where transaction will be executed */ - extra: HexString; - + genesisHash: Bytes(32), /** - * "Implicit" data to sign (known by the chain, not included into the extrinsic body). - * SCALE-encoded per the extension's "additionalSigned" type as defined in the metadata. + * SCALE-encoded Call (module indicator + function indicator + params). */ - additionalSigned: HexString; - }>; - - /** - * Transaction Extension Version. - * - For Extrinsic V4 MUST be 0. - * - For Extrinsic V5, set to any version supported by the runtime. - * The implementer: - * - MUST use this field to determine the required extensions for creating the extrinsic. - * - MAY use this field to infer missing extensions that the implementer could know how to handle. - */ - txExtVersion: number; - - /** - * Context needed for decoding, display, and (optionally) inferring certain extensions. - */ - context: { + callData: Bytes(), /** - * RuntimeMetadataPrefixed blob (SCALE), starting with ASCII "meta" magic (`0x6d657461`), - * then a metadata version (V14+). For V5+ versioned extensions, MUST provide V16+. + * Transaction extensions supplied by the caller (order irrelevant). + * The consumer SHOULD provide every extension that is relevant to them. + * The implementer MAY infer missing ones. */ - metadata: HexString; - + extensions: Vector(TxPayloadExtensionV1), /** - * Native token display info (used by some implementers), also needed to compute - * the `CheckMetadataHash` value. + * Transaction Extension Version. + * - For Extrinsic V4 MUST be 0. + * - For Extrinsic V5, set to any version supported by the runtime. + * The implementer: + * - MUST use this field to determine the required extensions for creating the extrinsic. + * - MAY use this field to infer missing extensions that the implementer could know how to handle. */ - tokenSymbol: string; - tokenDecimals: number; - - /** - * Highest known block number to aid mortality UX. - */ - bestBlockHeight: number; - }; + txExtVersion: u8, + }); } + +// transaction in the context of a host api account model + +export const ProductAccountTransaction = GenericTxPayloadV1(ProductAccountId); +export const LegacyTransaction = GenericTxPayloadV1(AccountId); + +export const CreateTransactionV1_request = ProductAccountTransaction; +export const CreateTransactionV1_response = Result(Bytes(), CreateTransactionErr); + +export const CreateTransactionWithLegacyAccountV1_request = LegacyTransaction; +export const CreateTransactionWithLegacyAccountV1_response = Result(Bytes(), CreateTransactionErr); diff --git a/packages/host-api/src/protocol/v1/notification.ts b/packages/host-api/src/protocol/v1/notification.ts index 685f9ae8..4841a766 100644 --- a/packages/host-api/src/protocol/v1/notification.ts +++ b/packages/host-api/src/protocol/v1/notification.ts @@ -1,11 +1,23 @@ -import { Option, Result, Struct, _void, str } from 'scale-ts'; +import { ErrEnum } from '@novasamatech/scale'; +import { Option, Result, Struct, _void, str, u32, u64 } from 'scale-ts'; -import { GenericError } from '../commonCodecs.js'; +import { GenericErr, GenericError } from '../commonCodecs.js'; + +export const NotificationId = u32; export const PushNotification = Struct({ text: str, deeplink: Option(str), + scheduledAt: Option(u64), +}); + +export const PushNotificationError = ErrEnum('PushNotificationError', { + ScheduleLimitReached: [_void, 'Schedule limit reached'], + Unknown: [GenericErr, 'Unknown error'], }); export const PushNotificationV1_request = PushNotification; -export const PushNotificationV1_response = Result(_void, GenericError); +export const PushNotificationV1_response = Result(NotificationId, PushNotificationError); + +export const PushNotificationCancelV1_request = NotificationId; +export const PushNotificationCancelV1_response = Result(_void, GenericError); diff --git a/packages/host-chat/package.json b/packages/host-chat/package.json index aff0a550..c7974f9e 100644 --- a/packages/host-chat/package.json +++ b/packages/host-chat/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-chat", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "private": "true", "description": "Host statement store chat integration", "license": "Apache-2.0", @@ -41,9 +41,9 @@ "README.md" ], "dependencies": { - "@novasamatech/scale": "0.7.8", - "@novasamatech/statement-store": "0.7.8", - "@novasamatech/storage-adapter": "0.7.8", + "@novasamatech/scale": "0.7.9", + "@novasamatech/statement-store": "0.7.9", + "@novasamatech/storage-adapter": "0.7.9", "nanoid": "5.1.9", "neverthrow": "^8.2.0" }, diff --git a/packages/host-container/package.json b/packages/host-container/package.json index ee006602..35dc0d8f 100644 --- a/packages/host-container/package.json +++ b/packages/host-container/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-container", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Host container for hosting and managing products within the Polkadot ecosystem.", "license": "Apache-2.0", "repository": { @@ -28,7 +28,7 @@ "@noble/hashes": "2.2.0", "polkadot-api": ">=2", "@polkadot-api/substrate-client": "^0.7.0", - "@novasamatech/host-api": "0.7.8", + "@novasamatech/host-api": "0.7.9", "nanoevents": "9.1.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0" diff --git a/packages/host-container/src/createContainer.ts b/packages/host-container/src/createContainer.ts index 4e8c2d7c..5b6dac8e 100644 --- a/packages/host-container/src/createContainer.ts +++ b/packages/host-container/src/createContainer.ts @@ -27,6 +27,7 @@ import { PaymentStatusErr, PaymentTopUpErr, PreimageSubmitErr, + PushNotificationError, RemotePermission, RequestCredentialsErr, ResourceAllocationErr, @@ -395,6 +396,12 @@ export function createContainer(provider: Provider, options: CreateContainerOpti const handlePushNotificationSlot = makeDevicePermissionGatedRequestSlot( 'host_push_notification', 'Notifications', + () => new PushNotificationError.Unknown({ reason: NOT_IMPLEMENTED }), + ); + + const handlePushNotificationCancelSlot = makeDevicePermissionGatedRequestSlot( + 'host_push_notification_cancel', + 'Notifications', () => new GenericError({ reason: NOT_IMPLEMENTED }), ); @@ -508,6 +515,14 @@ export function createContainer(provider: Provider, options: CreateContainerOpti handlePushNotification(handler) { return handleV1Request( handlePushNotificationSlot, + () => new PushNotificationError.Unknown({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR }), + handler, + ); + }, + + handlePushNotificationCancel(handler) { + return handleV1Request( + handlePushNotificationCancelSlot, () => new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR }), handler, ); diff --git a/packages/host-container/src/types.ts b/packages/host-container/src/types.ts index 25a0dee3..cb5c1153 100644 --- a/packages/host-container/src/types.ts +++ b/packages/host-container/src/types.ts @@ -97,6 +97,7 @@ export type Container = { handleDevicePermission: InferHandler<'v1', HostApiProtocol['host_device_permission']>; handlePermission: InferHandler<'v1', HostApiProtocol['remote_permission']>; handlePushNotification: InferHandler<'v1', HostApiProtocol['host_push_notification']>; + handlePushNotificationCancel: InferHandler<'v1', HostApiProtocol['host_push_notification_cancel']>; handleNavigateTo: InferHandler<'v1', HostApiProtocol['host_navigate_to']>; // entropy derivation diff --git a/packages/host-papp-react-ui/package.json b/packages/host-papp-react-ui/package.json index 87b36454..8cef571e 100644 --- a/packages/host-papp-react-ui/package.json +++ b/packages/host-papp-react-ui/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-papp-react-ui", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Polkadot app UI Flow", "license": "Apache-2.0", "repository": { @@ -30,18 +30,19 @@ "react-dom": ">=18" }, "dependencies": { - "@novasamatech/host-papp": "0.7.8", - "@novasamatech/statement-store": "0.7.8", + "@novasamatech/host-papp": "0.7.9", + "@novasamatech/statement-store": "0.7.9", "@polkadot-api/utils": "^0.4.0", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-popover": "1.1.15", - "qr-code-styling": "1.9.2", - "@novasamatech/tr-ui": "0.2.7" + "qrcode": "^1.5.4", + "@novasamatech/tr-ui": "0.2.9" }, "devDependencies": { + "@types/qrcode": "^1.5.6", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "@polkadot-api/substrate-bindings": "^0.20.0", + "@polkadot-api/substrate-bindings": "^0.20.2", "react": "19.2.5", "react-dom": "19.2.5", "typescript-plugin-css-modules": "5.2.0", diff --git a/packages/host-papp-react-ui/src/flow/Pairing.module.css b/packages/host-papp-react-ui/src/flow/Pairing.module.css index 48015c64..1ca743b5 100644 --- a/packages/host-papp-react-ui/src/flow/Pairing.module.css +++ b/packages/host-papp-react-ui/src/flow/Pairing.module.css @@ -8,7 +8,9 @@ } .container { - width: 350px; + box-sizing: border-box; + width: min(calc(100vw - 32px), 420px); + max-width: 420px; } .genericText { @@ -23,7 +25,10 @@ flex-direction: column; align-items: center; justify-content: center; - padding: 20px 50px 30px; + box-sizing: border-box; + width: 100%; + padding: 36px 44px 44px; + gap: 20px; } .pairingHeader { @@ -34,7 +39,6 @@ font-weight: 400; line-height: 1.83; letter-spacing: -0.573px; - margin-bottom: 9px; color: var(--color-text-foreground-inverse); } @@ -46,19 +50,38 @@ font-style: normal; font-weight: 400; line-height: 1.5; - margin-bottom: 15px; } .pairingDescription { font-family: Inter, sans-serif; - width: 300px; + box-sizing: border-box; + width: 100%; + max-width: 360px; color: var(--color-text-foreground-inverse, hsl(var(--muted-foreground))); text-align: center; font-size: 16px; font-style: normal; font-weight: 400; line-height: 1.2; - margin-top: 20px; +} + +.qrSurface { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + border-radius: 24px; + padding: 16px; +} + +.qrSurfaceLight { + composes: qrSurface; + background: var(--color-neutral-white); +} + +.qrSurfaceDark { + composes: qrSurface; + background: var(--color-neutral-black); } /* loader */ @@ -87,28 +110,27 @@ align-items: center; justify-content: space-between; gap: 40px; - padding: 30px; + width: 100%; + padding: 40px 44px 44px; box-sizing: border-box; } -.loaderHeader { +.loaderText { color: var(--color-text-foreground-inverse); text-align: center; font-family: Inter, sans-serif; font-size: 22px; - line-height: 1.5; /* 183.034% */ + line-height: 1.5; letter-spacing: -0.526px; } -.loaderText { - color: var(--color-text-foreground-inverse); - text-align: center; - font-family: Inter, sans-serif; - font-size: 29.203px; - font-style: normal; - font-weight: 400; - line-height: 53.452px; /* 183.034% */ - letter-spacing: -0.526px; +.loaderContainerPopover { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 40px; + padding: 20px; } /* finished */ @@ -189,7 +211,8 @@ gap: 12px; height: 100%; box-sizing: border-box; - padding: 40px 30px; + width: 100%; + padding: 40px 44px 44px; } .errorTitle { @@ -200,7 +223,6 @@ font-weight: 400; line-height: 1.83; letter-spacing: -0.573px; - margin-bottom: 9px; color: var(--color-text-foreground-inverse); } @@ -282,24 +304,6 @@ color: var(--color-text-foreground-inverse); } -.loaderContainerPopover { - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - gap: 40px; - padding: 20px; -} - -.loaderText { - color: var(--color-text-foreground-inverse); - text-align: center; - font-family: Inter, sans-serif; - font-size: 20px; - line-height: 1.5; - letter-spacing: -0.526px; -} - .retryButton { font-family: Inter, sans-serif; font-size: 16px; diff --git a/packages/host-papp-react-ui/src/flow/PairingModal.tsx b/packages/host-papp-react-ui/src/flow/PairingModal.tsx index c1486103..6ee99d52 100644 --- a/packages/host-papp-react-ui/src/flow/PairingModal.tsx +++ b/packages/host-papp-react-ui/src/flow/PairingModal.tsx @@ -22,7 +22,7 @@ export const PairingModal = memo(({ theme, size = 280 }: Props = {}) => { const open = auth.authUIMode === 'modal' && status.step !== 'none' && status.step !== 'finished'; const toggleModal = (newOpen: boolean) => { - if (!newOpen && status.step !== 'attestation') { + if (!newOpen && status.step !== 'pending') { auth.abortAuthentication(); } }; @@ -37,9 +37,8 @@ export const PairingModal = memo(({ theme, size = 280 }: Props = {}) => {
{status.step === 'pairing' && } + {status.step === 'pending' && } {status.step === 'pairingError' && } - {status.step === 'attestation' && } - {status.step === 'attestationError' && }
); @@ -47,27 +46,27 @@ export const PairingModal = memo(({ theme, size = 280 }: Props = {}) => { const PairingStep = ({ payload, size, theme }: { payload: string; size: number; theme?: 'light' | 'dark' }) => { const translation = useTranslations(); + const isDark = theme === 'dark'; return (
{translation.pairingHeader} {translation.pairingScanCallToAction} - +
+ +
{translation.pairingDescription}
); }; -const LoadingStep = () => { - const translation = useTranslations(); - +const LoadingStep = ({ stage }: { stage: string }) => { return (
- {translation.pairingLoginMessage}
- {translation.pairingLoader} + {stage}
); }; diff --git a/packages/host-papp-react-ui/src/flow/PairingPopover.tsx b/packages/host-papp-react-ui/src/flow/PairingPopover.tsx index 4e6b159f..41dd991f 100644 --- a/packages/host-papp-react-ui/src/flow/PairingPopover.tsx +++ b/packages/host-papp-react-ui/src/flow/PairingPopover.tsx @@ -40,7 +40,7 @@ export const PairingPopover = memo( const togglePopover = useCallback( (newOpen: boolean) => { - if (!newOpen && status.step !== 'attestation') { + if (!newOpen && status.step !== 'pending') { auth.abortAuthentication(); } }, @@ -51,7 +51,7 @@ export const PairingPopover = memo( return () => { auth.abortAuthentication(); }; - }, [auth]); + }, [auth.abortAuthentication]); return ( @@ -66,21 +66,20 @@ export const PairingPopover = memo( alignOffset={alignOffset} className={styles.popoverContent} onInteractOutside={e => { - if (status.step === 'attestation') { + if (status.step === 'pending') { e.preventDefault(); } }} onEscapeKeyDown={e => { - if (status.step === 'attestation') { + if (status.step === 'pending') { e.preventDefault(); } }} >
{status.step === 'pairing' && } + {status.step === 'pending' && } {status.step === 'pairingError' && } - {status.step === 'attestation' && } - {status.step === 'attestationError' && }
@@ -99,27 +98,27 @@ const PairingStep = ({ theme?: 'light' | 'dark'; }) => { const translation = useTranslations(); + const isDark = theme === 'dark'; return (
{translation.pairingPopoverWelcome} - +
+ +
{translation.pairingPopoverLoginHeading} {translation.pairingPopoverScanDescription}
); }; -const LoadingStep = () => { - const translation = useTranslations(); - +const LoadingStep = ({ stage }: { stage: string }) => { return (
- {translation.pairingPopoverWelcome}
- {translation.pairingLoader} + {stage}
); }; diff --git a/packages/host-papp-react-ui/src/hooks/authStatus.ts b/packages/host-papp-react-ui/src/hooks/authStatus.ts index 7f611211..209a2d10 100644 --- a/packages/host-papp-react-ui/src/hooks/authStatus.ts +++ b/packages/host-papp-react-ui/src/hooks/authStatus.ts @@ -3,10 +3,7 @@ import { useMemo } from 'react'; import { useAuthentication } from '../providers/AuthProvider.js'; export const useAuthStatus = () => { - const auth = useAuthentication(); - const { pairingStatus, attestationStatus } = auth; - - const guestUsername = attestationStatus.step === 'attestation' ? attestationStatus.username : null; + const { pairingStatus } = useAuthentication(); const signedInUser = useMemo(() => { if (pairingStatus.step === 'finished') { @@ -15,28 +12,8 @@ export const useAuthStatus = () => { return null; }, [pairingStatus.step]); - const status = useMemo(() => { - if (pairingStatus.step === 'none') { - return pairingStatus; - } - if (pairingStatus.step === 'initial') { - return pairingStatus; - } - if (pairingStatus.step === 'pairing') { - return pairingStatus; - } - if (attestationStatus.step === 'attestation') { - return attestationStatus; - } - if (attestationStatus.step === 'attestationError') { - return attestationStatus; - } - return pairingStatus; - }, [pairingStatus, attestationStatus]); - return { - status, - guestUsername, + status: pairingStatus, signedInUser, }; }; diff --git a/packages/host-papp-react-ui/src/providers/AuthProvider.tsx b/packages/host-papp-react-ui/src/providers/AuthProvider.tsx index 25a34931..9d565273 100644 --- a/packages/host-papp-react-ui/src/providers/AuthProvider.tsx +++ b/packages/host-papp-react-ui/src/providers/AuthProvider.tsx @@ -1,4 +1,4 @@ -import type { AttestationStatus, PairingStatus, UserSession } from '@novasamatech/host-papp'; +import type { PairingStatus, UserSession } from '@novasamatech/host-papp'; import { toastError } from '@novasamatech/tr-ui'; import type { PropsWithChildren } from 'react'; import { createContext, useCallback, useContext, useDebugValue, useState, useSyncExternalStore } from 'react'; @@ -18,7 +18,6 @@ export function withRetry(fn: () => Promise, maxRetries = 2): Promise { type Auth = { pairingStatus: PairingStatus; - attestationStatus: AttestationStatus; pending: boolean; authUIMode: AuthUIMode; authenticate(ui?: AuthUIMode): Promise; @@ -28,7 +27,6 @@ type Auth = { const Context = createContext({ pairingStatus: { step: 'none' }, - attestationStatus: { step: 'none' }, pending: false, authUIMode: null, authenticate: () => Promise.resolve(), @@ -53,25 +51,12 @@ const usePairingStatus = () => { return pairingStatus; }; -const useAttestationStatus = () => { - const provider = usePapp(); - const attestationStatus = useSyncExternalStore( - provider.sso.attestationStatus.subscribe, - provider.sso.attestationStatus.read, - ); - - useDebugValue(`Polkadot app attestation status: ${attestationStatus.step}`); - - return attestationStatus; -}; - export const AuthProvider = ({ children }: PropsWithChildren) => { const [pending, setPending] = useState(false); const [authUIMode, setAuthUIMode] = useState(null); const provider = usePapp(); const pairingStatus = usePairingStatus(); - const attestationStatus = useAttestationStatus(); const authenticate = useCallback( (ui?: AuthUIMode) => { @@ -112,7 +97,6 @@ export const AuthProvider = ({ children }: PropsWithChildren) => { const state: Auth = { pending, pairingStatus, - attestationStatus, authUIMode, authenticate, abortAuthentication, diff --git a/packages/host-papp-react-ui/src/providers/TranslationProvider.tsx b/packages/host-papp-react-ui/src/providers/TranslationProvider.tsx index b00b23a8..95118a45 100644 --- a/packages/host-papp-react-ui/src/providers/TranslationProvider.tsx +++ b/packages/host-papp-react-ui/src/providers/TranslationProvider.tsx @@ -5,12 +5,9 @@ export type Translations = { pairingHeader: string; pairingScanCallToAction: string; pairingDescription: string; - pairingLoader: string; - pairingAttestationError: string; pairingRetry: string; pairingError: string; pairingWelcomeMessage: string; - pairingLoginMessage: string; pairingPopoverWelcome: string; pairingPopoverLoginHeading: string; pairingPopoverScanDescription: string; @@ -26,12 +23,9 @@ const defaultKeys: TranslationsMap = { pairingScanCallToAction: 'Scan it with a phone', pairingDescription: 'Scanning the QR code opens the Polkadot mobile app, where users are guided step-by-step through the onboarding or setup process.', - pairingLoader: 'Just a second...', - pairingAttestationError: 'Error while passing attestation', pairingRetry: 'Retry', pairingError: 'Error while pairing', pairingWelcomeMessage: 'Welcome back,', - pairingLoginMessage: 'Loggin in', pairingPopoverWelcome: 'Welcome to Polkadot!', pairingPopoverLoginHeading: 'Login to explore all Polkadot features', pairingPopoverScanDescription: 'Scan with your phone camera to log in using the Polkadot mobile app', diff --git a/packages/host-papp-react-ui/src/ui/Modal.stories.tsx b/packages/host-papp-react-ui/src/ui/Modal.stories.tsx index d60c4075..5896a030 100644 --- a/packages/host-papp-react-ui/src/ui/Modal.stories.tsx +++ b/packages/host-papp-react-ui/src/ui/Modal.stories.tsx @@ -32,7 +32,7 @@ export const Default: Story = { children: (
{/* @ts-expect-error nullable args */} - +
), }, diff --git a/packages/host-papp-react-ui/src/ui/QrCode.module.css b/packages/host-papp-react-ui/src/ui/QrCode.module.css index 54f5f24a..c74f36e5 100644 --- a/packages/host-papp-react-ui/src/ui/QrCode.module.css +++ b/packages/host-papp-react-ui/src/ui/QrCode.module.css @@ -16,7 +16,61 @@ display: flex; justify-content: center; align-items: center; - padding: 16px; + box-sizing: border-box; + padding: 0; +} + +.qrFrame { + position: relative; + display: inline-block; + line-height: 0; +} + +.qrCanvas { + display: block; + image-rendering: pixelated; +} + +.qrCanvasLight { + composes: qrCanvas; + color: var(--color-neutral-black); +} + +.qrCanvasDark { + composes: qrCanvas; + color: var(--color-neutral-white); +} + +.logoBackdrop { + position: absolute; + left: 50%; + top: 50%; + box-sizing: border-box; + display: flex; + width: 13%; + height: 13%; + align-items: center; + justify-content: center; + border-radius: 50%; + transform: translate(-50%, -50%); + pointer-events: none; +} + +.logoBackdropLight { + composes: logoBackdrop; + background: var(--color-neutral-white); +} + +.logoBackdropDark { + composes: logoBackdrop; + background: var(--color-neutral-black); +} + +.logo { + display: block; + width: 82%; + height: 82%; + object-fit: contain; } .gradientLayer { diff --git a/packages/host-papp-react-ui/src/ui/QrCode.tsx b/packages/host-papp-react-ui/src/ui/QrCode.tsx index ab81641a..4b9daa44 100644 --- a/packages/host-papp-react-ui/src/ui/QrCode.tsx +++ b/packages/host-papp-react-ui/src/ui/QrCode.tsx @@ -1,82 +1,139 @@ -import _QRCodeStyling from 'qr-code-styling'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useEffect, useRef } from 'react'; import styles from './QrCode.module.css'; -const QRCodeStyling = _QRCodeStyling as unknown as typeof _QRCodeStyling.default; - type Props = { value: string; size: number; theme?: 'light' | 'dark'; }; -const COLOR_LIGHT = '#000000'; -const COLOR_DARK = '#ffffff'; - -const IMAGE_LIGHT = +/** Black mark on transparent SVG — on white circular island (light theme: black-on-white matrix). */ +const LOGO_LIGHT_BASE64 = 'PHN2ZyB3aWR0aD0iNDMiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0MyA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIuNDIxMzggMTAuMTkyM0MtMC43MzQyNTUgMTMuODcxNyAtMC44MTc0NjcgMTguOTkzNCAyLjI0MjE1IDIxLjYyMTVDNS4zMDE3NyAyNC4yNTYgMTAuMzM5MyAyMy40MDM1IDEzLjUwMTMgMTkuNzE3N0MxNi42NTY5IDE2LjAzODMgMTYuNzQwMSAxMC45MTY3IDEzLjY4MDUgOC4yODg1NEMxMi40ODM2IDcuMjU2NTIgMTAuOTczIDYuNzYyOTQgOS4zOTgzNCA2Ljc2Mjk0QzYuOTUzMiA2Ljc2Mjk0IDQuMzQxNjQgNy45NTUyMSAyLjQyMTM4IDEwLjE5MjNaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMS41MDYxNCAyOS4yMzA0Qy0wLjg2ODU4NCAzMi4wNTA4IDAuMTYxOTU2IDM2LjgzOTIgMy44MTA0NiAzOS45MjI0QzcuNDU4OTYgNDMuMDA1NyAxMi4zNDkyIDQzLjIyMzYgMTQuNzI0IDQwLjQwMzJDMTcuMDk4NyAzNy41ODI3IDE2LjA2ODEgMzIuNzk0NCAxMi40MTk2IDI5LjcxMTJDMTAuNDg2NiAyOC4wNzY2IDguMjA3ODYgMjcuMjQ5NyA2LjE0MDM4IDI3LjI0OTdDNC4zMDMzMyAyNy4yNDk3IDIuNjI2MyAyNy45MDM1IDEuNTEyNTQgMjkuMjMwNCIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTIyLjkyOTcgMzkuNTUwNkMxOC42NDc1IDQwLjkwMzIgMTUuNjk2NyA0My42NTk1IDE2LjMzNjggNDUuNzA0M0MxNi45ODMzIDQ3Ljc0OTEgMjAuOTc3NCA0OC4zMTMyIDI1LjI1OTYgNDYuOTU0M0MyOS41NDE4IDQ1LjYwMTcgMzIuNDkyNiA0Mi44NDU0IDMxLjg1MjUgNDAuODAwNkMzMS40NDI4IDM5LjUxMjIgMjkuNzAxOCAzOC44MDcxIDI3LjM5NzUgMzguODA3MUMyNi4wNDY5IDM4LjgwNzEgMjQuNTEwNyAzOS4wNDQyIDIyLjkyOTcgMzkuNTQ0MiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTE2LjIyOCAyLjYyODE5QzE1LjM0NDcgNS4xNzk0IDE4LjE0ODMgOC40NzQxOCAyMi41MDA5IDkuOTkzMzdDMjYuODUzNSAxMS41MTI2IDMxLjA5NzMgMTAuNjcyOCAzMS45ODA2IDguMTIxNjNDMzIuODYzOSA1LjU3MDQyIDMwLjA2MDMgMi4yNzU2NCAyNS43MDc3IDAuNzU2NDUyQzI0LjI0MTkgMC4yNDM2NDYgMjIuNzgyNSA2LjIyMDk2ZS0wNSAyMS40NjQgNi4xOTc5MWUtMDVDMTguODcxNiA2LjE1MjU4ZS0wNSAxNi44MTA1IDAuOTM1OTMzIDE2LjIyOCAyLjYyODE5WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTM1Ljc5NiA3Ljc2OTc5QzM0LjUzNTEgOC4yNzYxOCAzNC44MzU5IDExLjk2MiAzNi40NjE3IDE1Ljk4NzVDMzguMDg3NSAyMC4wMTk0IDQwLjQyMzkgMjIuODcxOSA0MS42ODQ4IDIyLjM2NTVDNDIuOTM5NCAyMS44NTkxIDQyLjY0NSAxOC4xNzk4IDQxLjAxOTEgMTQuMTQ3OEMzOS41MjEzIDEwLjQzIDM3LjQxNTQgNy43MTIxIDM2LjEwOTcgNy43MTIxQzM2LjAwMDkgNy43MTIxIDM1Ljg5ODQgNy43MzEzMyAzNS43OTYgNy43Njk3OVoiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0zNi43NjE5IDMyLjI2MjZDMzQuOTY5NyAzNi4xNTM1IDM0LjM0ODggMzkuNjk4MyAzNS4zNzkzIDQwLjE3MjZDMzYuNDA5OSA0MC42NDcgMzguNzAxNCAzNy44Nzc4IDQwLjQ5MzYgMzMuOTg2OUM0Mi4yOTIzIDMwLjA5NiA0Mi45MDY4IDI2LjU1MTIgNDEuODgyNiAyNi4wNzY5QzQxLjgwNTggMjYuMDM4NCA0MS43MjI2IDI2LjAyNTYgNDEuNjI2NiAyNi4wMjU2QzQwLjUwNjQgMjYuMDI1NiAzOC40MTk4IDI4LjY2NjUgMzYuNzYxOSAzMi4yNjI2WiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg=='; -const IMAGE_DARK = +/** White mark on transparent SVG — on dark circular island (dark theme: white-on-dark matrix). */ +const LOGO_DARK_BASE64 = 'PHN2ZyB3aWR0aD0iNTgiIGhlaWdodD0iNjUiIHZpZXdCb3g9IjAgMCA1OCA2NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMy4yNjM5NyAxMy43MjY1Qy0wLjk4OTc3MiAxOC42ODE3IC0xLjEwMTk0IDI1LjU3OTIgMy4wMjIzNyAyOS4xMTg3QzcuMTQ2NjkgMzIuNjY2NyAxMy45MzcxIDMxLjUxODYgMTguMTk5NSAyNi41NTQ3QzIyLjQ1MzIgMjEuNTk5NSAyMi41NjU0IDE0LjcwMiAxOC40NDExIDExLjE2MjZDMTYuODI3NiA5Ljc3MjcxIDE0Ljc5MTMgOS4xMDc5OSAxMi42Njg4IDkuMTA3OTlDOS4zNzI3NyA5LjEwNzk5IDUuODUyNDUgMTAuNzEzNyAzLjI2Mzk3IDEzLjcyNjVaIiBmaWxsPSJ3aGl0ZSIvPjxwYXRoIGQ9Ik0yLjAzMDAxIDM5LjM2NTdDLTEuMTcxMDggNDMuMTY0MSAwLjIxODA2NiA0OS42MTI4IDUuMTM2MTggNTMuNzY1MUMxMC4wNTQzIDU3LjkxNzQgMTYuNjQ2MyA1OC4yMTEgMTkuODQ3NCA1NC40MTI2QzIzLjA0ODUgNTAuNjE0MiAyMS42NTkzIDQ0LjE2NTUgMTYuNzQxMiA0MC4wMTMyQzE0LjEzNTUgMzcuODExOCAxMS4wNjM4IDM2LjY5ODIgOC4yNzY4NyAzNi42OTgyQzUuODAwNTUgMzYuNjk4MiAzLjUzOTk2IDM3LjU3ODcgMi4wMzg2NCAzOS4zNjU3IiBmaWxsPSJ3aGl0ZSIvPjxwYXRoIGQ9Ik0zMC45MDg3IDUzLjI2NDNDMjUuMTM2NCA1NS4wODU5IDIxLjE1ODggNTguNzk3OSAyMi4wMjE2IDYxLjU1MThDMjIuODkzMSA2NC4zMDU2IDI4LjI3NzEgNjUuMDY1MyAzNC4wNDk0IDYzLjIzNTFDMzkuODIxNyA2MS40MTM2IDQzLjc5OTQgNTcuNzAxNiA0Mi45MzY2IDU0Ljk0NzdDNDIuMzg0MyA1My4yMTI2IDQwLjAzNzUgNTIuMjYzIDM2LjkzMTMgNTIuMjYzQzM1LjExMDcgNTIuMjYzIDMzLjAzOTkgNTIuNTgyNCAzMC45MDg3IDUzLjI1NTciIGZpbGw9IndoaXRlIi8+PHBhdGggZD0iTTIxLjg3NDkgMy41Mzk0MkMyMC42ODQyIDYuOTc1MjQgMjQuNDYzNCAxMS40MTI1IDMwLjMzMDYgMTMuNDU4NEMzNi4xOTc5IDE1LjUwNDQgNDEuOTE4NCAxNC4zNzM1IDQzLjEwOTEgMTAuOTM3N0M0NC4yOTk4IDcuNTAxODQgNDAuNTIwNiAzLjA2NDYyIDM0LjY1MzQgMS4wMTg2NkMzMi42Nzc1IDAuMzI4MDQ0IDMwLjcxMDMgMCAyOC45MzI5IDBDMjUuNDM4NCAwIDIyLjY2MDEgMS4yNjAzOCAyMS44NzQ5IDMuNTM5NDJaIiBmaWxsPSJ3aGl0ZSIvPjxwYXRoIGQ9Ik00OC4yNTE2IDEwLjQ2MzhDNDYuNTUxOSAxMS4xNDU4IDQ2Ljk1NzQgMTYuMTA5NiA0OS4xNDkgMjEuNTMwOUM1MS4zNDA2IDI2Ljk2MDkgNTQuNDg5OSAzMC44MDI1IDU2LjE4OTcgMzAuMTIwNUM1Ny44ODA4IDI5LjQzODUgNTcuNDgzOSAyNC40ODMzIDU1LjI5MjMgMTkuMDUzNEM1My4yNzMzIDE0LjA0NjQgNTAuNDM0NiAxMC4zODYxIDQ4LjY3NDQgMTAuMzg2MUM0OC41Mjc3IDEwLjM4NjEgNDguMzg5NyAxMC40MTIgNDguMjUxNiAxMC40NjM4WiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNNDkuNTU0NCA0My40NDkxQzQ3LjEzODUgNDguNjg5MiA0Ni4zMDE1IDUzLjQ2MzEgNDcuNjkwNyA1NC4xMDE5QzQ5LjA3OTggNTQuNzQwNyA1Mi4xNjg4IDUxLjAxMTQgNTQuNTg0NyA0NS43NzEzQzU3LjAwOTIgNDAuNTMxMyA1Ny44Mzc2IDM1Ljc1NzQgNTYuNDU3IDM1LjExODVDNTYuMzUzNSAzNS4wNjY3IDU2LjI0MTMgMzUuMDQ5NSA1Ni4xMTE5IDM1LjA0OTVDNTQuNjAyIDM1LjA0OTUgNTEuNzg5MSAzOC42MDYyIDQ5LjU1NDQgNDMuNDQ5MVoiIGZpbGw9IndoaXRlIi8+PC9zdmc+'; +type BitMatrix = { size: number; data: Uint8Array }; + +const QUIET_ZONE = 4; +const FINDER_SIZE = 7; + +function renderQrToCanvas(canvas: HTMLCanvasElement, modules: BitMatrix, pxSize: number, matrixColor: string) { + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + const dpr = window.devicePixelRatio || 1; + const totalModules = modules.size + QUIET_ZONE * 2; + // Snap cell size to an integer device-pixel grid so data modules stay crisp. + const cellSizeDevice = Math.max(1, Math.floor((pxSize * dpr) / totalModules)); + const canvasSizeDevice = cellSizeDevice * totalModules; + + canvas.width = canvasSizeDevice; + canvas.height = canvasSizeDevice; + canvas.style.width = `${pxSize}px`; + canvas.style.height = `${pxSize}px`; + + ctx.clearRect(0, 0, canvasSizeDevice, canvasSizeDevice); + + const moduleCount = modules.size; + const isInFinder = (row: number, col: number) => + (row < FINDER_SIZE && col < FINDER_SIZE) || + (row < FINDER_SIZE && col >= moduleCount - FINDER_SIZE) || + (row >= moduleCount - FINDER_SIZE && col < FINDER_SIZE); + + ctx.fillStyle = matrixColor; + for (let row = 0; row < moduleCount; row++) { + for (let col = 0; col < moduleCount; col++) { + if (!modules.data[row * moduleCount + col]) { + continue; + } + if (isInFinder(row, col)) { + continue; + } + const x = (col + QUIET_ZONE) * cellSizeDevice; + const y = (row + QUIET_ZONE) * cellSizeDevice; + ctx.fillRect(x, y, cellSizeDevice, cellSizeDevice); + } + } + + const drawFinderCircle = (row: number, col: number) => { + const cx = (col + FINDER_SIZE / 2 + QUIET_ZONE) * cellSizeDevice; + const cy = (row + FINDER_SIZE / 2 + QUIET_ZONE) * cellSizeDevice; + + ctx.fillStyle = matrixColor; + ctx.beginPath(); + ctx.arc(cx, cy, 3.5 * cellSizeDevice, 0, Math.PI * 2); + ctx.fill(); + + ctx.globalCompositeOperation = 'destination-out'; + ctx.beginPath(); + ctx.arc(cx, cy, 2.5 * cellSizeDevice, 0, Math.PI * 2); + ctx.fill(); + ctx.globalCompositeOperation = 'source-over'; + + ctx.fillStyle = matrixColor; + ctx.beginPath(); + ctx.arc(cx, cy, 1.5 * cellSizeDevice, 0, Math.PI * 2); + ctx.fill(); + }; + + drawFinderCircle(0, 0); + drawFinderCircle(0, moduleCount - FINDER_SIZE); + drawFinderCircle(moduleCount - FINDER_SIZE, 0); +} + export const QrCode = memo(({ value, size, theme = 'light' }: Props) => { - const [ref, setRef] = useState(null); + const canvasRef = useRef(null); const isDark = theme === 'dark'; - const color = isDark ? COLOR_DARK : COLOR_LIGHT; - const backgroundColor = isDark ? COLOR_LIGHT : COLOR_DARK; - const imageBase64 = isDark ? IMAGE_DARK : IMAGE_LIGHT; - - const qrCode = useMemo(() => { - return new QRCodeStyling({ - data: value, - type: 'svg', - shape: 'square', - width: size, - height: size, - margin: 0, - image: imageBase64 ? `data:image/svg+xml;base64,${imageBase64}` : undefined, - imageOptions: { - hideBackgroundDots: true, - imageSize: 0.4, - margin: 6, - }, - dotsOptions: { - type: 'rounded', - color, - }, - backgroundOptions: { - color: 'transparent', - }, - cornersSquareOptions: { - type: 'dot', - color, - }, - cornersDotOptions: { - type: 'dot', - color, - }, - qrOptions: { - errorCorrectionLevel: 'M', - }, - }); - }, [size, color, imageBase64]); + const logoBase64 = isDark ? LOGO_DARK_BASE64 : LOGO_LIGHT_BASE64; + const logoDataUrl = `data:image/svg+xml;base64,${logoBase64}`; useEffect(() => { - if (ref) { - qrCode.append(ref); + const canvas = canvasRef.current; + if (!canvas || !value) { + return; } + let cancelled = false; + const capturedValue = value; + const matrixColor = getComputedStyle(canvas).color; + + void import('qrcode') + .then(QRCode => { + if (cancelled) { + return; + } + const qr = QRCode.default.create(capturedValue, { errorCorrectionLevel: 'H' }); + renderQrToCanvas(canvas, qr.modules, size, matrixColor); + }) + .catch((err: unknown) => { + if (!cancelled) { + console.error('[host-papp-react-ui] QR render failed:', err); + } + }); + return () => { - if (ref) { - QRCodeStyling._clearContainer(ref); - } + cancelled = true; }; - }, [qrCode, ref]); + }, [value, size, isDark]); - useEffect(() => { - qrCode?.update({ data: value }); - }, [value, qrCode]); + if (!value) { + return
; + } - return
; + return ( +
+
+ +
+ +
+
+
+ ); }); diff --git a/packages/host-papp/__tests__/attestationService.spec.ts b/packages/host-papp/__tests__/attestationService.spec.ts deleted file mode 100644 index c2997582..00000000 --- a/packages/host-papp/__tests__/attestationService.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -vi.mock('verifiablejs/bundler', () => ({ - member_from_entropy: vi.fn(() => new Uint8Array(32)), - sign: vi.fn(() => new Uint8Array(64)), -})); - -vi.mock('polkadot-api/signer', () => ({ - getPolkadotSigner: vi.fn(() => ({ - publicKey: new Uint8Array(32), - signBytes: vi.fn(), - signTx: vi.fn(), - })), -})); - -vi.mock('../src/crypto.js', async importOriginal => { - const actual = await importOriginal(); - return { - ...actual, - getEncrPub: vi.fn(() => new Uint8Array(65)), - }; -}); - -import { onHostPappDebugMessage } from '../src/debugBus.js'; -import type { AttestationDebugEvent } from '../src/debugTypes.js'; -import { createAttestationService, withRetry } from '../src/sso/auth/attestationService.js'; - -describe('withRetry', () => { - it('resolves immediately when fn succeeds on first call', async () => { - const fn = vi.fn().mockResolvedValue('ok'); - - const result = await withRetry(fn); - - expect(result).toBe('ok'); - expect(fn).toHaveBeenCalledTimes(1); - }); - - it('retries once and resolves when fn fails then succeeds', async () => { - const fn = vi.fn().mockRejectedValueOnce(new Error('Stale')).mockResolvedValueOnce('ok'); - - const result = await withRetry(fn); - - expect(result).toBe('ok'); - expect(fn).toHaveBeenCalledTimes(2); - }); - - it('rejects after exhausting all retries', async () => { - const error = new Error('Stale'); - const fn = vi.fn().mockRejectedValue(error); - - await expect(withRetry(fn)).rejects.toThrow('Stale'); - expect(fn).toHaveBeenCalledTimes(2); // 1 initial + 1 retry - }); - - it('respects custom maxRetries', async () => { - const error = new Error('Stale'); - const fn = vi.fn().mockRejectedValue(error); - - await expect(withRetry(fn, 3)).rejects.toThrow('Stale'); - expect(fn).toHaveBeenCalledTimes(4); // 1 initial + 3 retries - }); - - it('retries up to maxRetries and resolves on last attempt', async () => { - const fn = vi - .fn() - .mockRejectedValueOnce(new Error('fail 1')) - .mockRejectedValueOnce(new Error('fail 2')) - .mockResolvedValueOnce('recovered'); - - const result = await withRetry(fn, 2); - - expect(result).toBe('recovered'); - expect(fn).toHaveBeenCalledTimes(3); - }); - - it('does not retry when maxRetries is 0', async () => { - const error = new Error('Stale'); - const fn = vi.fn().mockRejectedValue(error); - - await expect(withRetry(fn, 0)).rejects.toThrow('Stale'); - expect(fn).toHaveBeenCalledTimes(1); - }); - - it('preserves the last error when all retries fail', async () => { - const fn = vi.fn().mockRejectedValueOnce(new Error('first')).mockRejectedValueOnce(new Error('second')); - - await expect(withRetry(fn, 1)).rejects.toThrow('second'); - }); - - it('propagates non-Error rejections', async () => { - const fn = vi.fn().mockRejectedValue('string error'); - - await expect(withRetry(fn, 0)).rejects.toBe('string error'); - }); -}); - -describe('createAttestationService', () => { - function createMockAccount() { - return { - secret: new Uint8Array(64) as any, - publicKey: new Uint8Array(32) as any, - entropy: new Uint8Array(32), - sign: vi.fn(() => new Uint8Array(64)), - verify: vi.fn(() => true), - }; - } - - describe('grantVerifierAllowance', () => { - function makeService(opts: { allowance: number; signAndSubmit: ReturnType }) { - const mockApi = { - query: { - PeopleLite: { - AttestationAllowance: { getValue: vi.fn().mockResolvedValue(opts.allowance) }, - }, - }, - tx: { - PeopleLite: { - increase_attestation_allowance: vi.fn(() => ({ decodedCall: {} })), - }, - Sudo: { - sudo: vi.fn(() => ({ signAndSubmit: opts.signAndSubmit })), - }, - }, - }; - const lazyClient = { getClient: () => ({ getUnsafeApi: () => mockApi }) } as any; - return createAttestationService(lazyClient); - } - - it('retries signAndSubmit on failure and resolves on the second attempt', async () => { - const signAndSubmit = vi.fn().mockRejectedValueOnce(new Error('Stale')).mockResolvedValueOnce(undefined); - const service = makeService({ allowance: 0, signAndSubmit }); - - const result = await service.grantVerifierAllowance(createMockAccount()); - - expect(result.isOk()).toBe(true); - expect(signAndSubmit).toHaveBeenCalledTimes(2); - }); - - it('fails after retries are exhausted', async () => { - const signAndSubmit = vi.fn().mockRejectedValue(new Error('Stale')); - const service = makeService({ allowance: 0, signAndSubmit }); - - const result = await service.grantVerifierAllowance(createMockAccount()); - - expect(result.isErr()).toBe(true); - expect(signAndSubmit).toHaveBeenCalledTimes(2); - }); - - it('skips the transaction when allowance is already sufficient', async () => { - const signAndSubmit = vi.fn(); - const service = makeService({ allowance: 5, signAndSubmit }); - - const result = await service.grantVerifierAllowance(createMockAccount()); - - expect(result.isOk()).toBe(true); - expect(signAndSubmit).not.toHaveBeenCalled(); - }); - }); - - describe('debug emits', () => { - const FLOW_ID = 'flow-attestation-test'; - - function captureAttestationEvents() { - const events: AttestationDebugEvent[] = []; - const unsubscribe = onHostPappDebugMessage(event => { - if (event.layer === 'attestation') events.push(event); - }); - return { events, unsubscribe }; - } - - function makeRegisterableService() { - const subscribeSpy = vi.fn( - (handlers: { next: (event: { type: string; found?: boolean; ok?: boolean }) => void }) => { - // defer next() so the `subscription` binding inside the production code - // is in scope by the time `subscription.unsubscribe()` is called - queueMicrotask(() => handlers.next({ type: 'finalized', ok: true })); - return { unsubscribe: vi.fn() }; - }, - ); - const mockApi = { - query: { - PeopleLite: { - AttestationAllowance: { getValue: vi.fn().mockResolvedValue(10) }, - }, - }, - tx: { - PeopleLite: { - increase_attestation_allowance: vi.fn(() => ({ decodedCall: {} })), - attest: vi.fn(() => ({ signSubmitAndWatch: () => ({ subscribe: subscribeSpy }) })), - }, - Sudo: { - sudo: vi.fn(() => ({ signAndSubmit: vi.fn() })), - }, - }, - }; - const lazyClient = { getClient: () => ({ getUnsafeApi: () => mockApi }) } as any; - return createAttestationService(lazyClient, FLOW_ID); - } - - it('claimUsername emits username_claimed', () => { - const { events, unsubscribe } = captureAttestationEvents(); - try { - const service = makeRegisterableService(); - const username = service.claimUsername(); - expect(events).toContainEqual( - expect.objectContaining({ - event: 'username_claimed', - flowId: FLOW_ID, - payload: { username }, - }), - ); - } finally { - unsubscribe(); - } - }); - - it('grantVerifierAllowance emits allowance_granted on success', async () => { - const { events, unsubscribe } = captureAttestationEvents(); - try { - const service = makeRegisterableService(); - const result = await service.grantVerifierAllowance(createMockAccount()); - expect(result.isOk()).toBe(true); - expect(events.some(e => e.event === 'allowance_granted' && e.flowId === FLOW_ID)).toBe(true); - } finally { - unsubscribe(); - } - }); - - it('deriveAttestationParams emits vrf_proof_generated', async () => { - const { events, unsubscribe } = captureAttestationEvents(); - try { - const service = makeRegisterableService(); - const result = await service.deriveAttestationParams('guest.0001', createMockAccount(), createMockAccount()); - expect(result.isOk()).toBe(true); - expect(events.some(e => e.event === 'vrf_proof_generated' && e.flowId === FLOW_ID)).toBe(true); - } finally { - unsubscribe(); - } - }); - - it('registerLitePerson emits person_registered after successful submission', async () => { - const { events, unsubscribe } = captureAttestationEvents(); - try { - const service = makeRegisterableService(); - const result = await service.registerLitePerson('guest.0001', createMockAccount(), createMockAccount()); - expect(result.isOk()).toBe(true); - const personRegistered = events.find(e => e.event === 'person_registered'); - expect(personRegistered).toBeDefined(); - expect(personRegistered?.flowId).toBe(FLOW_ID); - expect(personRegistered?.payload).toMatchObject({ username: 'guest.0001' }); - } finally { - unsubscribe(); - } - }); - - it('omits emits entirely when no debugFlowId is provided', () => { - const { events, unsubscribe } = captureAttestationEvents(); - try { - // service constructed without flowId — must not emit anything - const mockApi = { - query: { PeopleLite: { AttestationAllowance: { getValue: vi.fn().mockResolvedValue(10) } } }, - tx: { PeopleLite: { increase_attestation_allowance: vi.fn() }, Sudo: { sudo: vi.fn() } }, - }; - const lazyClient = { getClient: () => ({ getUnsafeApi: () => mockApi }) } as any; - const service = createAttestationService(lazyClient); - service.claimUsername(); - expect(events).toHaveLength(0); - } finally { - unsubscribe(); - } - }); - }); -}); diff --git a/packages/host-papp/__tests__/auth.spec.ts b/packages/host-papp/__tests__/auth.spec.ts index 9c6d67d0..de5b2eba 100644 --- a/packages/host-papp/__tests__/auth.spec.ts +++ b/packages/host-papp/__tests__/auth.spec.ts @@ -1,5 +1,5 @@ -import type { LazyClient, Statement, StatementStoreAdapter } from '@novasamatech/statement-store'; -import { errAsync, ok, okAsync } from 'neverthrow'; +import type { Statement, StatementStoreAdapter } from '@novasamatech/statement-store'; +import { ok, okAsync } from 'neverthrow'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { onHostPappDebugMessage } from '../src/debugBus.js'; @@ -9,9 +9,6 @@ import type { UserSecretRepository } from '../src/sso/userSecretRepository.js'; import type { UserSessionRepository } from '../src/sso/userSessionRepository.js'; const mocks = vi.hoisted(() => ({ - grantVerifierAllowance: vi.fn(), - registerLitePerson: vi.fn(), - claimUsername: vi.fn(), decrypt: vi.fn(), generateMnemonic: vi.fn(), handshakeEnc: vi.fn(), @@ -44,21 +41,6 @@ vi.mock('../src/crypto.js', async importOriginal => { }; }); -vi.mock('../src/sso/auth/attestationService.js', () => ({ - createAttestationService: vi.fn(() => ({ - claimUsername: mocks.claimUsername, - grantVerifierAllowance: mocks.grantVerifierAllowance, - registerLitePerson: mocks.registerLitePerson, - })), - createSudoAliceVerifier: vi.fn(() => ({ - secret: new Uint8Array(64), - publicKey: new Uint8Array(32), - entropy: new Uint8Array(32), - sign: vi.fn(), - verify: vi.fn(() => true), - })), -})); - vi.mock('../src/sso/auth/scale/handshake.js', () => ({ HandshakeData: { enc: mocks.handshakeEnc }, HandshakeResponsePayload: { dec: mocks.responsePayloadDec }, @@ -108,7 +90,6 @@ function buildHarness() { const statementStore = { subscribeStatements }; const ssoSessionRepository = { add: vi.fn(() => okAsync(undefined)) }; const userSecretRepository = { write: vi.fn(() => okAsync(undefined)) }; - const lazyClient = { getClient: () => ({ getUnsafeApi: () => ({}) }) }; const auth = createAuth({ metadata: 'test-metadata', @@ -116,7 +97,6 @@ function buildHarness() { statementStore: statementStore as unknown as StatementStoreAdapter, ssoSessionRepository: ssoSessionRepository as unknown as UserSessionRepository, userSecretRepository: userSecretRepository as unknown as UserSecretRepository, - lazyClient: lazyClient as unknown as LazyClient, }); return { @@ -141,9 +121,6 @@ function buildHarness() { } beforeEach(() => { - mocks.grantVerifierAllowance.mockReset().mockReturnValue(okAsync(undefined)); - mocks.registerLitePerson.mockReset().mockReturnValue(okAsync(undefined)); - mocks.claimUsername.mockReset().mockReturnValue('guestabcd.1234'); mocks.decrypt.mockReset().mockReturnValue(ok(new Uint8Array([7, 7, 7]))); mocks.generateMnemonic.mockReset().mockReturnValue('test mnemonic'); mocks.handshakeEnc.mockReset().mockReturnValue(new Uint8Array([0xab, 0xcd])); @@ -160,10 +137,9 @@ beforeEach(() => { describe('createAuth', () => { describe('initial state', () => { - it('starts with both statuses at "none"', () => { + it('starts with pairingStatus at "none"', () => { const { auth } = buildHarness(); expect(auth.pairingStatus.read()).toEqual({ step: 'none' }); - expect(auth.attestationStatus.read()).toEqual({ step: 'none' }); }); }); @@ -233,23 +209,6 @@ describe('createAuth', () => { expect(steps.at(-1)).toBe('finished'); }); - it('emits attestationStatus transitions: none -> attestation(username) -> finished', async () => { - const harness = buildHarness(); - const { auth } = harness; - const observed: Array<{ step: string; username?: string }> = []; - auth.attestationStatus.subscribe(s => observed.push(s as never)); - - const promise = auth.authenticate(); - await harness.waitForSubscription(); - harness.deliverHandshake(); - await promise; - - expect(observed[0]?.step).toBe('none'); - const attestation = observed.find(s => s.step === 'attestation'); - expect(attestation).toEqual({ step: 'attestation', username: 'guestabcd.1234' }); - expect(observed.at(-1)?.step).toBe('finished'); - }); - it('skips statements with no data and resolves on the first decryptable one', async () => { const harness = buildHarness(); const { auth, ssoSessionRepository } = harness; @@ -265,24 +224,6 @@ describe('createAuth', () => { }); describe('authenticate (error paths)', () => { - it('publishes attestationError and rejects when registration fails', async () => { - mocks.registerLitePerson.mockReturnValue(errAsync(new Error('chain offline'))); - const harness = buildHarness(); - const { auth } = harness; - - const promise = auth.authenticate(); - await harness.waitForSubscription(); - harness.deliverHandshake(); - const result = await promise; - - expect(result.isErr()).toBe(true); - expect(result._unsafeUnwrapErr().message).toBe('chain offline'); - expect(auth.attestationStatus.read()).toEqual({ - step: 'attestationError', - message: 'chain offline', - }); - }); - it('publishes pairingError when retrieving the session throws', async () => { mocks.responsePayloadDec.mockImplementation(() => { throw new Error('payload broken'); @@ -317,27 +258,6 @@ describe('createAuth', () => { }); }); - it('clears the cached result after failure so the next call retries', async () => { - mocks.registerLitePerson.mockReturnValueOnce(errAsync(new Error('boom'))).mockReturnValue(okAsync(undefined)); - - const harness = buildHarness(); - const { auth } = harness; - - const first = auth.authenticate(); - await harness.waitForSubscription(); - harness.deliverHandshake(); - await first; - - const second = auth.authenticate(); - expect(second).not.toBe(first); - expect(mocks.generateMnemonic).toHaveBeenCalledTimes(2); - - auth.abortAuthentication(); - await harness.waitForSubscription(2); - harness.deliverPage([]); - await second; - }); - it('does not persist secrets or session when handshake fails', async () => { mocks.handshakeEnc.mockImplementation(() => { throw new Error('encode broken'); @@ -368,7 +288,7 @@ describe('createAuth', () => { expect(result._unsafeUnwrap()).toBeNull(); }); - it('resets pairing and attestation statuses', async () => { + it('resets pairing status', async () => { const harness = buildHarness(); const { auth } = harness; @@ -379,16 +299,13 @@ describe('createAuth', () => { await promise; expect(auth.pairingStatus.read()).toEqual({ step: 'none' }); - expect(auth.attestationStatus.read()).toEqual({ step: 'none' }); }); - it('does not transition pairingStatus or attestationStatus to error states on user abort', async () => { + it('does not transition pairingStatus to error state on user abort', async () => { const harness = buildHarness(); const { auth } = harness; const pairing: Array<{ step: string }> = []; - const attestation: Array<{ step: string }> = []; auth.pairingStatus.subscribe(s => pairing.push(s as never)); - auth.attestationStatus.subscribe(s => attestation.push(s as never)); const promise = auth.authenticate(); await harness.waitForSubscription(); @@ -397,7 +314,6 @@ describe('createAuth', () => { await promise; expect(pairing.some(s => s.step === 'pairingError')).toBe(false); - expect(attestation.some(s => s.step === 'attestationError')).toBe(false); }); it('clears the cached result so the next call starts a fresh attempt', async () => { @@ -424,7 +340,6 @@ describe('createAuth', () => { const { auth } = buildHarness(); expect(() => auth.abortAuthentication()).not.toThrow(); expect(auth.pairingStatus.read()).toEqual({ step: 'none' }); - expect(auth.attestationStatus.read()).toEqual({ step: 'none' }); }); }); @@ -444,7 +359,6 @@ describe('createAuth', () => { expect(events.find(e => e.layer === 'sso' && e.event === 'pairing_started')).toMatchObject({ payload: { metadata: 'test-metadata' }, }); - expect(events.some(e => e.layer === 'attestation' && e.event === 'started')).toBe(true); } finally { auth.abortAuthentication(); unsubscribe(); @@ -469,48 +383,6 @@ describe('createAuth', () => { 'response_received', 'session_established', ]); - expect(events.find(e => e.layer === 'attestation' && e.event === 'completed')).toBeDefined(); - } finally { - unsubscribe(); - } - }); - - it('shares one flowId across every event in a single pairing run', async () => { - const harness = buildHarness(); - const { events, unsubscribe } = captureEvents(); - try { - const promise = harness.auth.authenticate(); - await harness.waitForSubscription(); - harness.deliverHandshake(); - await promise; - - const ssoFlowIds = new Set(events.filter(e => e.layer === 'sso').map(e => e.flowId)); - expect(ssoFlowIds.size).toBe(1); - - // attestation runs in its own flow, distinct from the SSO pairing flow - const attestationFlowIds = new Set(events.filter(e => e.layer === 'attestation').map(e => e.flowId)); - expect(attestationFlowIds.size).toBe(1); - expect(attestationFlowIds).not.toEqual(ssoFlowIds); - } finally { - unsubscribe(); - } - }); - - it('emits pairing_failed and attestation.failed when the chain rejects with a non-abort error', async () => { - mocks.registerLitePerson.mockReturnValue(errAsync(new Error('chain offline'))); - const harness = buildHarness(); - const { events, unsubscribe } = captureEvents(); - try { - const promise = harness.auth.authenticate(); - await harness.waitForSubscription(); - harness.deliverHandshake(); - const result = await promise; - - expect(result.isErr()).toBe(true); - const pairingFailed = events.find(e => e.layer === 'sso' && e.event === 'pairing_failed'); - const attestationFailed = events.find(e => e.layer === 'attestation' && e.event === 'failed'); - expect(pairingFailed?.payload).toMatchObject({ reason: 'chain offline' }); - expect(attestationFailed?.payload).toMatchObject({ reason: 'chain offline' }); } finally { unsubscribe(); } diff --git a/packages/host-papp/package.json b/packages/host-papp/package.json index e665b5e4..cbdc88fd 100644 --- a/packages/host-papp/package.json +++ b/packages/host-papp/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-papp", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Polkadot app integration", "license": "Apache-2.0", "repository": { @@ -34,17 +34,16 @@ "@noble/ciphers": "2.2.0", "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", - "@novasamatech/host-api": "0.7.8", - "@novasamatech/scale": "0.7.8", - "@novasamatech/statement-store": "0.7.8", - "@novasamatech/storage-adapter": "0.7.8", + "@novasamatech/host-api": "0.7.9", + "@novasamatech/scale": "0.7.9", + "@novasamatech/statement-store": "0.7.9", + "@novasamatech/storage-adapter": "0.7.9", "@polkadot-labs/hdkd-helpers": "^0.0.30", "nanoevents": "9.1.0", "nanoid": "5.1.9", "neverthrow": "^8.2.0", "polkadot-api": ">=2", - "scale-ts": "1.6.1", - "verifiablejs": "1.2.0" + "scale-ts": "1.6.1" }, "publishConfig": { "access": "public" diff --git a/packages/host-papp/src/index.ts b/packages/host-papp/src/index.ts index febf3050..2bcb821d 100644 --- a/packages/host-papp/src/index.ts +++ b/packages/host-papp/src/index.ts @@ -4,15 +4,15 @@ export type { PappAdapter } from './papp.js'; export { createPappAdapter } from './papp.js'; export type { HostMetadata } from './sso/auth/impl.js'; -export type { AttestationStatus, PairingStatus } from './sso/auth/types.js'; +export type { PairingStatus } from './sso/auth/types.js'; export type { UserSession } from './sso/sessionManager/userSession.js'; export type { StoredUserSession } from './sso/userSessionRepository.js'; export type { Identity } from './identity/types.js'; export type { SigningPayloadRequest, + SigningPayloadResponse, SigningRawRequest, SigningRequest, -} from './sso/sessionManager/scale/signingRequest.js'; -export type { SigningPayloadResponse } from './sso/sessionManager/scale/signingResponse.js'; +} from './sso/sessionManager/scale/signing.js'; export type { RingVrfAliasRequest, RingVrfAliasResponse } from './sso/sessionManager/scale/ringVrf.js'; diff --git a/packages/host-papp/src/papp.ts b/packages/host-papp/src/papp.ts index 611c87cb..55b8c931 100644 --- a/packages/host-papp/src/papp.ts +++ b/packages/host-papp/src/papp.ts @@ -74,7 +74,6 @@ export function createPappAdapter({ appId, metadata, hostMetadata, adapters }: P statementStore, ssoSessionRepository, userSecretRepository, - lazyClient, }), sessions: createSsoSessionManager({ storage, statementStore, ssoSessionRepository, userSecretRepository }), secrets: userSecretRepository, diff --git a/packages/host-papp/src/sso/auth/attestationService.ts b/packages/host-papp/src/sso/auth/attestationService.ts deleted file mode 100644 index 97b18ada..00000000 --- a/packages/host-papp/src/sso/auth/attestationService.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { toHex } from '@novasamatech/scale'; -import type { LazyClient } from '@novasamatech/statement-store'; -import { createAccountId } from '@novasamatech/statement-store'; -import { blake2b256 } from '@polkadot-labs/hdkd-helpers'; -import { customAlphabet } from 'nanoid'; -import type { ResultAsync } from 'neverthrow'; -import { errAsync, fromAsyncThrowable, fromPromise, okAsync } from 'neverthrow'; -import { AccountId, Binary } from 'polkadot-api'; -import type { PolkadotSigner } from 'polkadot-api/signer'; -import { getPolkadotSigner } from 'polkadot-api/signer'; -import { mergeUint8 } from 'polkadot-api/utils'; -import { Bytes, Option, Tuple, str } from 'scale-ts'; -import { member_from_entropy, sign } from 'verifiablejs/bundler'; - -import type { People_lite } from '../../../.papi/descriptors/dist/index.js'; -import type { DerivedSr25519Account, EncrSecret } from '../../crypto.js'; -import { deriveSr25519Account, getEncrPub, stringToBytes } from '../../crypto.js'; -import { emitHostPappDebugMessage } from '../../debugBus.js'; -import { toError } from '../../helpers/utils.js'; - -const accountId = AccountId(); - -export function createSudoAliceVerifier(): DerivedSr25519Account { - return deriveSr25519Account('bottom drive obey lake curtain smoke basket hold race lonely fit walk', '//Alice'); -} - -export function withRetry(fn: () => Promise, maxRetries = 1): Promise { - return fn().catch(error => { - if (maxRetries > 0) { - return withRetry(fn, maxRetries - 1); - } - throw error; - }); -} - -export const createAttestationService = (lazyClient: LazyClient, debugFlowId?: string) => { - const service = { - claimUsername() { - const nameSuffixFactory = customAlphabet('abcdefghijklmnopqrstuvwxyz', 4); - - const username = `guest${nameSuffixFactory()}.${createNumericSuffix(4)}`; - if (debugFlowId !== undefined) { - emitHostPappDebugMessage({ - layer: 'attestation', - event: 'username_claimed', - flowId: debugFlowId, - timestamp: Date.now(), - payload: { username }, - }); - } - return username; - }, - - grantVerifierAllowance(verifier: DerivedSr25519Account): ResultAsync { - const client = lazyClient.getClient(); - const api = client.getUnsafeApi(); - const verifierAddress = accountId.dec(verifier.publicKey); - - if (!api.query.PeopleLite || !api.query.PeopleLite.AttestationAllowance) { - return errAsync(new Error('Query PeopleLite.AttestationAllowance not found.')); - } - - const verifierAllowance = fromPromise( - api.query.PeopleLite.AttestationAllowance.getValue(verifierAddress), - toError, - ); - - const getAllowance = fromAsyncThrowable(async (): Promise => { - const increaseAllowanceCall = api.tx.PeopleLite.increase_attestation_allowance({ - account: verifierAddress, - count: 10, - }); - - const sudoCall = api.tx.Sudo.sudo({ - call: increaseAllowanceCall.decodedCall, - }); - - return withRetry(() => sudoCall.signAndSubmit(createPeopleSigner(verifier)).then(() => undefined)); - }, toError); - - return verifierAllowance - .andThen(verifierAllowance => (verifierAllowance > 0 ? okAsync() : getAllowance())) - .andTee(() => { - if (debugFlowId !== undefined) { - emitHostPappDebugMessage({ - layer: 'attestation', - event: 'allowance_granted', - flowId: debugFlowId, - timestamp: Date.now(), - payload: { verifierAccountId: verifierAddress }, - }); - } - }); - }, - - getRingRfKey(candidate: DerivedSr25519Account) { - const verifiableEntropy = blake2b256(candidate.entropy); - return member_from_entropy(verifiableEntropy); - }, - - getProofMessage(candidate: DerivedSr25519Account, ringVrfKey: Uint8Array) { - return mergeUint8([stringToBytes('pop:people-lite:register using'), candidate.publicKey, ringVrfKey]); - }, - - deriveAttestationParams(username: string, candidate: DerivedSr25519Account, verifier: DerivedSr25519Account) { - const verifiableEntropy = blake2b256(candidate.entropy); - const ringVrfKey = service.getRingRfKey(candidate); - const identifierKey = getEncrPub(blake2b256(candidate.secret) as EncrSecret); - - const message = service.getProofMessage(candidate, ringVrfKey); - - // Extract username without the `.` separator and any following digits - // For lite person usernames like "ceainnhgidpj.39642086", we only use "ceainnhgidpj" - const usernameWithoutDigits = username.split('.')[0] ?? username; - - const candidateSignature = candidate.sign(message); - const proofOfOwnership = sign(verifiableEntropy, message); - - if (debugFlowId !== undefined) { - emitHostPappDebugMessage({ - layer: 'attestation', - event: 'vrf_proof_generated', - flowId: debugFlowId, - timestamp: Date.now(), - payload: { candidateAccountId: accountId.dec(candidate.publicKey) }, - }); - } - - const ResourceSignatureCodec = Tuple( - // candidate PublicKey (32 bytes) - Bytes(32), - // verifier AccountId (32 bytes) - Bytes(32), - // identifierKey - Bytes(65), - // username without digits - str, - // reserved_username - Option(Bytes()), - ); - - const resourcesSignatureData = ResourceSignatureCodec.enc([ - candidate.publicKey, - createAccountId(verifier.publicKey), - identifierKey, - usernameWithoutDigits, - undefined, - ]); - - const consumerRegistrationSignature = candidate.sign(resourcesSignatureData); - - return okAsync({ - candidateSignature: candidateSignature, - ringVrfKey, - proofOfOwnership, - identifierKey, - consumerRegistrationSignature, - }); - }, - - registerLitePerson(username: string, candidate: DerivedSr25519Account, verifier: DerivedSr25519Account) { - const client = lazyClient.getClient(); - const api = client.getUnsafeApi(); - - return service - .deriveAttestationParams(username, candidate, verifier) - .andThen(params => { - const attestCall = api.tx.PeopleLite.attest({ - candidate: accountId.dec(candidate.publicKey), - candidate_signature: { - type: 'Sr25519', - value: toHex(params.candidateSignature), - }, - ring_vrf_key: toHex(params.ringVrfKey), - proof_of_ownership: toHex(params.proofOfOwnership), - consumer_registration: { - signature: { - type: 'Sr25519', - value: toHex(params.consumerRegistrationSignature), - }, - account: accountId.dec(candidate.publicKey), - identifier_key: toHex(params.identifierKey), - username: Binary.fromText(username), - reserved_username: undefined, - }, - }); - - const submitAttestation = () => - new Promise((resolve, reject) => { - const subscription = attestCall.signSubmitAndWatch(createPeopleSigner(verifier)).subscribe({ - next(event: { - type: string; - found?: boolean; - ok?: boolean; - dispatchError?: { type: string; value?: unknown }; - }) { - if ((event.type === 'txBestBlocksState' && event.found) || event.type === 'finalized') { - subscription.unsubscribe(); - if (event.ok) { - resolve(); - } else { - let errorMessage = 'Transaction failed'; - if (event.dispatchError?.type === 'Module') { - const moduleError = event.dispatchError.value as { type: string; value?: { type?: string } }; - errorMessage = `${moduleError.type}.${moduleError.value?.type ?? 'Unknown'}`; - } - reject(new Error(errorMessage)); - } - } - }, - error: reject, - complete: () => reject(new Error('Transaction observable completed without best block confirmation')), - }); - }); - - return fromPromise(withRetry(submitAttestation), toError).map(() => undefined); - }) - .andTee(() => { - console.log(`Attestation for ${accountId.dec(candidate.publicKey)} successfully passed.`); - if (debugFlowId !== undefined) { - emitHostPappDebugMessage({ - layer: 'attestation', - event: 'person_registered', - flowId: debugFlowId, - timestamp: Date.now(), - payload: { - username, - candidateAccountId: accountId.dec(candidate.publicKey), - }, - }); - } - }); - }, - }; - - return service; -}; - -function createNumericSuffix(length: number) { - let suffix = ''; - for (let i = 0; i < length; i++) { - suffix += (Math.random() * 9).toFixed(); - } - return suffix; -} - -function createPeopleSigner(verifier: DerivedSr25519Account): PolkadotSigner { - const baseSigner = getPolkadotSigner(verifier.publicKey, 'Sr25519', verifier.sign); - - return { - publicKey: baseSigner.publicKey, - signBytes: baseSigner.signBytes, - signTx: async (callData, signedExtensions, metadata, atBlockNumber, hasher) => { - // polkadot-api auto-derives all signed extensions whose default state is encodable as - // a zero byte (Option::None, struct(Option<_>) = None, etc.). The People runtime's - // `pallet_verify_signature::VerifySignature` ({ Disabled, Signed }) extension uses - // identifier "VerifyMultiSignature" but has no built-in handler in polkadot-api, so it - // must be supplied manually. Value 0x00 = `Disabled` (passthrough — the standard - // extrinsic signature authenticates the call). Variant tag order was flipped in - // polkadot-sdk#11897 specifically so generic signers can encode the passthrough state - // as a single zero byte. - const extensionsWithCustom = { - ...signedExtensions, - VerifyMultiSignature: { - identifier: 'VerifyMultiSignature', - value: new Uint8Array([0]), - additionalSigned: new Uint8Array([]), - }, - }; - - return baseSigner.signTx(callData, extensionsWithCustom, metadata, atBlockNumber, hasher); - }, - }; -} diff --git a/packages/host-papp/src/sso/auth/impl.ts b/packages/host-papp/src/sso/auth/impl.ts index 949abf3b..cba57855 100644 --- a/packages/host-papp/src/sso/auth/impl.ts +++ b/packages/host-papp/src/sso/auth/impl.ts @@ -1,5 +1,5 @@ import { enumValue } from '@novasamatech/scale'; -import type { LazyClient, LocalSessionAccount, Statement, StatementStoreAdapter } from '@novasamatech/statement-store'; +import type { LocalSessionAccount, Statement, StatementStoreAdapter } from '@novasamatech/statement-store'; import { createAccountId, createEncryption, @@ -22,9 +22,8 @@ import type { UserSecretRepository } from '../userSecretRepository.js'; import type { StoredUserSession, UserSessionRepository } from '../userSessionRepository.js'; import { createStoredUserSession } from '../userSessionRepository.js'; -import { createAttestationService, createSudoAliceVerifier } from './attestationService.js'; import { HandshakeData, HandshakeResponsePayload, HandshakeResponseSensitiveData } from './scale/handshake.js'; -import type { AttestationStatus, PairingStatus } from './types.js'; +import type { PairingStatus } from './types.js'; export type AuthComponent = ReturnType; @@ -40,7 +39,6 @@ type Params = { statementStore: StatementStoreAdapter; ssoSessionRepository: UserSessionRepository; userSecretRepository: UserSecretRepository; - lazyClient: LazyClient; }; export function createAuth({ @@ -49,61 +47,12 @@ export function createAuth({ statementStore, ssoSessionRepository, userSecretRepository, - lazyClient, }: Params) { - const attestationStatus = createState({ step: 'none' }); const pairingStatus = createState({ step: 'none' }); let authResult: ResultAsync | null = null; let abort: AbortController | null = null; - function attestAccount(account: DerivedSr25519Account, signal: AbortSignal) { - const flowId = createFlowId(); - const candidateAccountId = toHex(account.publicKey); - emitHostPappDebugMessage({ - layer: 'attestation', - event: 'started', - flowId, - timestamp: Date.now(), - payload: { candidateAccountId }, - }); - - const attestationService = createAttestationService(lazyClient, flowId); - - const verifier = createSudoAliceVerifier(); - const username = attestationService.claimUsername(); - - attestationStatus.write({ step: 'attestation', username }); - - return attestationService - .grantVerifierAllowance(verifier) - .andThrough(() => processSignal(signal)) - .andThen(() => attestationService.registerLitePerson(username, account, verifier)) - .andThrough(() => processSignal(signal)) - .andTee(() => { - attestationStatus.write({ step: 'finished' }); - emitHostPappDebugMessage({ - layer: 'attestation', - event: 'completed', - flowId, - timestamp: Date.now(), - payload: { username }, - }); - }) - .orTee(e => { - if (!(e instanceof AbortError)) { - attestationStatus.write({ step: 'attestationError', message: e.message }); - emitHostPappDebugMessage({ - layer: 'attestation', - event: 'failed', - flowId, - timestamp: Date.now(), - payload: { reason: e.message }, - }); - } - }); - } - function handshake(account: DerivedSr25519Account, signal: AbortSignal, flowId: string) { const localAccount = createLocalSessionAccount(createAccountId(account.publicKey)); @@ -193,7 +142,6 @@ export function createAuth({ const authModule = { pairingStatus: readonly(pairingStatus), - attestationStatus: readonly(attestationStatus), authenticate(): ResultAsync { if (authResult) { @@ -212,13 +160,8 @@ export function createAuth({ payload: { metadata }, }); - authResult = ResultAsync.combine([ - handshake(account, abort.signal, ssoFlowId), - attestAccount(account, abort.signal), - ]) - .andThen(([handshakeResult]) => { - // Save secrets and sso session only after attestation has finished - const { session, secretsPayload } = handshakeResult; + authResult = handshake(account, abort.signal, ssoFlowId) + .andThen(({ session, secretsPayload }) => { return userSecretRepository .write(secretsPayload.id, { ssSecret: secretsPayload.ssSecret, @@ -265,7 +208,6 @@ export function createAuth({ } authResult = null; pairingStatus.reset(); - attestationStatus.reset(); }, }; diff --git a/packages/host-papp/src/sso/auth/types.ts b/packages/host-papp/src/sso/auth/types.ts index af9d0bde..421faca5 100644 --- a/packages/host-papp/src/sso/auth/types.ts +++ b/packages/host-papp/src/sso/auth/types.ts @@ -3,13 +3,7 @@ import type { StoredUserSession } from '../userSessionRepository.js'; export type PairingStatus = | { step: 'none' } | { step: 'initial' } - | { step: 'attestation' } | { step: 'pairing'; payload: string } + | { step: 'pending'; stage: string } | { step: 'pairingError'; message: string } | { step: 'finished'; session: StoredUserSession }; - -export type AttestationStatus = - | { step: 'none' } - | { step: 'attestation'; username: string } - | { step: 'attestationError'; message: string } - | { step: 'finished' }; diff --git a/packages/host-papp/src/sso/sessionManager/scale/createTransaction.ts b/packages/host-papp/src/sso/sessionManager/scale/createTransaction.ts new file mode 100644 index 00000000..728faf68 --- /dev/null +++ b/packages/host-papp/src/sso/sessionManager/scale/createTransaction.ts @@ -0,0 +1,18 @@ +import { ProductAccountTransaction } from '@novasamatech/host-api'; +import { Enum } from '@novasamatech/scale'; +import type { CodecType } from 'scale-ts'; +import { Bytes, Result, Struct, str } from 'scale-ts'; + +export type CreateTransactionRequest = CodecType; +export const CreateTransactionRequestCodec = Struct({ + payload: Enum({ + v1: ProductAccountTransaction, + }), +}); + +export type CreateTransactionResponse = CodecType; +export const CreateTransactionResponseCodec = Struct({ + // referencing to RemoteMessage.messageId + respondingTo: str, + signedTransaction: Result(Bytes(), str), +}); diff --git a/packages/host-papp/src/sso/sessionManager/scale/remoteMessage.ts b/packages/host-papp/src/sso/sessionManager/scale/remoteMessage.ts index de764f79..fde669bf 100644 --- a/packages/host-papp/src/sso/sessionManager/scale/remoteMessage.ts +++ b/packages/host-papp/src/sso/sessionManager/scale/remoteMessage.ts @@ -1,10 +1,10 @@ import type { CodecType } from 'scale-ts'; import { Enum, Struct, _void, str } from 'scale-ts'; +import { CreateTransactionRequestCodec, CreateTransactionResponseCodec } from './createTransaction.js'; import { ResourceAllocationRequestCodec, ResourceAllocationResponseCodec } from './resourceAllocation.js'; import { RingVrfAliasRequestCodec, RingVrfAliasResponseCodec } from './ringVrf.js'; -import { SigningRequestCodec } from './signingRequest.js'; -import { SigningResponseCodec } from './signingResponse.js'; +import { SigningRequestCodec, SigningResponseCodec } from './signing.js'; export type RemoteMessage = CodecType; export const RemoteMessageCodec = Struct({ @@ -18,6 +18,8 @@ export const RemoteMessageCodec = Struct({ RingVrfAliasResponse: RingVrfAliasResponseCodec, ResourceAllocationRequest: ResourceAllocationRequestCodec, ResourceAllocationResponse: ResourceAllocationResponseCodec, + CreateTransactionRequest: CreateTransactionRequestCodec, + CreateTransactionResponse: CreateTransactionResponseCodec, }), }), }); diff --git a/packages/host-papp/src/sso/sessionManager/scale/signingRequest.ts b/packages/host-papp/src/sso/sessionManager/scale/signing.ts similarity index 66% rename from packages/host-papp/src/sso/sessionManager/scale/signingRequest.ts rename to packages/host-papp/src/sso/sessionManager/scale/signing.ts index 59befbf5..8915f70d 100644 --- a/packages/host-papp/src/sso/sessionManager/scale/signingRequest.ts +++ b/packages/host-papp/src/sso/sessionManager/scale/signing.ts @@ -1,7 +1,7 @@ import { ProductAccountId } from '@novasamatech/host-api'; import { Enum, Hex, OptionBool } from '@novasamatech/scale'; import type { CodecType } from 'scale-ts'; -import { Bytes, Option, Struct, Vector, str, u32 } from 'scale-ts'; +import { Bytes, Option, Result, Struct, Vector, str, u32 } from 'scale-ts'; export type SigningPayloadRequest = CodecType; export const SigningPayloadRequestCodec = Struct({ @@ -37,3 +37,16 @@ export const SigningRequestCodec = Enum({ Payload: SigningPayloadRequestCodec, Raw: SigningRawRequestCodec, }); + +export type SigningPayloadResponseData = CodecType; +export const SigningPayloadResponseDataCodec = Struct({ + signature: Bytes(), + signedTransaction: Option(Bytes()), +}); + +export type SigningPayloadResponse = CodecType; +export const SigningResponseCodec = Struct({ + // referencing to RemoteMessage.messageId + respondingTo: str, + payload: Result(SigningPayloadResponseDataCodec, str), +}); diff --git a/packages/host-papp/src/sso/sessionManager/scale/signingResponse.ts b/packages/host-papp/src/sso/sessionManager/scale/signingResponse.ts deleted file mode 100644 index 7aa6f61e..00000000 --- a/packages/host-papp/src/sso/sessionManager/scale/signingResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CodecType } from 'scale-ts'; -import { Bytes, Option, Result, Struct, str } from 'scale-ts'; - -export type SigningPayloadResponseData = CodecType; -export const SigningPayloadResponseDataCodec = Struct({ - signature: Bytes(), - signedTransaction: Option(Bytes()), -}); - -export type SigningPayloadResponse = CodecType; -export const SigningResponseCodec = Struct({ - // referencing to RemoteMessage.messageId - respondingTo: str, - payload: Result(SigningPayloadResponseDataCodec, str), -}); diff --git a/packages/host-papp/src/sso/sessionManager/userSession.ts b/packages/host-papp/src/sso/sessionManager/userSession.ts index f3e19822..19879d94 100644 --- a/packages/host-papp/src/sso/sessionManager/userSession.ts +++ b/packages/host-papp/src/sso/sessionManager/userSession.ts @@ -15,11 +15,11 @@ import { toError } from '../../helpers/utils.js'; import type { Callback } from '../../types.js'; import type { StoredUserSession } from '../userSessionRepository.js'; +import type { CreateTransactionRequest } from './scale/createTransaction.js'; import type { RemoteMessage } from './scale/remoteMessage.js'; import { RemoteMessageCodec } from './scale/remoteMessage.js'; import type { ApAllocationOutcome, ResourceAllocationRequest } from './scale/resourceAllocation.js'; -import type { SigningPayloadRequest, SigningRawRequest } from './scale/signingRequest.js'; -import type { SigningPayloadResponseData } from './scale/signingResponse.js'; +import type { SigningPayloadRequest, SigningPayloadResponseData, SigningRawRequest } from './scale/signing.js'; // Timeout for the inner queue task. Without it the queue wedges forever when // the remote signer doesn't respond — e.g. the request @@ -97,6 +97,7 @@ export type UserSession = StoredUserSession & { sendDisconnectMessage(): ResultAsync; signPayload(payload: SigningPayloadRequest): ResultAsync; signRaw(payload: SigningRawRequest): ResultAsync; + createTransaction(payload: CreateTransactionRequest): ResultAsync; getRingVrfAlias( productAccountId: CodecType, productId: string, @@ -204,6 +205,38 @@ export function createUserSession({ }); }, + createTransaction(payload) { + return requestQueue.call(() => { + const messageId = nanoid(); + const request = session.request(RemoteMessageCodec, { + messageId, + data: enumValue('v1', enumValue('CreateTransactionRequest', payload)), + }); + + const responseFilter = (message: RemoteMessage) => { + if ( + message.data.tag === 'v1' && + message.data.value.tag === 'CreateTransactionResponse' && + message.data.value.value.respondingTo === messageId + ) { + return message.data.value.value.signedTransaction; + } + }; + + const inner = request + .andThen(() => session.waitForRequestMessage(RemoteMessageCodec, responseFilter)) + .andThen(message => { + if (message.success) { + return ok(message.value); + } else { + return err(new Error(message.value)); + } + }); + + return withQueueTimeout(inner, 'createTransaction'); + }); + }, + sendDisconnectMessage() { return requestQueue.call(() => session diff --git a/packages/host-substrate-chain-connection/package.json b/packages/host-substrate-chain-connection/package.json index b1f19c84..b2a3f895 100644 --- a/packages/host-substrate-chain-connection/package.json +++ b/packages/host-substrate-chain-connection/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-substrate-chain-connection", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Chain connection pool with ref counting and provider branching for Polkadot API", "license": "Apache-2.0", "repository": { @@ -25,7 +25,7 @@ "README.md" ], "dependencies": { - "@novasamatech/storage-adapter": "0.7.8", + "@novasamatech/storage-adapter": "0.7.9", "@polkadot-api/ws-provider": "^0.9.0", "@polkadot-api/json-rpc-provider": "^0.2.0", "@polkadot-api/json-rpc-provider-proxy": "^0.4.0", diff --git a/packages/host-worker-sandbox/package.json b/packages/host-worker-sandbox/package.json index f94f1144..00863295 100644 --- a/packages/host-worker-sandbox/package.json +++ b/packages/host-worker-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/host-worker-sandbox", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "QuickJS-based sandbox for running product worker code with Triangle Host API.", "license": "Apache-2.0", "repository": { @@ -25,8 +25,8 @@ "README.md" ], "dependencies": { - "@novasamatech/host-api": "0.7.8", - "@novasamatech/host-container": "0.7.8", + "@novasamatech/host-api": "0.7.9", + "@novasamatech/host-container": "0.7.9", "quickjs-emscripten": "0.32.0" }, "publishConfig": { diff --git a/packages/product-bulletin/package.json b/packages/product-bulletin/package.json index 6a322bed..d3b8f09d 100644 --- a/packages/product-bulletin/package.json +++ b/packages/product-bulletin/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/product-bulletin", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Bulletin Chain client adapter for Polkadot product applications", "license": "Apache-2.0", "repository": { @@ -27,7 +27,7 @@ "README.md" ], "dependencies": { - "@novasamatech/host-api-wrapper": "0.7.8", + "@novasamatech/host-api-wrapper": "0.7.9", "@parity/bulletin-sdk": "^0.3.0", "polkadot-api": ">=2" }, diff --git a/packages/product-react-renderer/package.json b/packages/product-react-renderer/package.json index 58b1af95..3d1142d4 100644 --- a/packages/product-react-renderer/package.json +++ b/packages/product-react-renderer/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/product-react-renderer", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "React wrapper for custom renderer format from host-api-wrapper", "license": "Apache-2.0", "repository": { @@ -24,8 +24,8 @@ "README.md" ], "dependencies": { - "@novasamatech/host-api": "0.7.8", - "@novasamatech/host-api-wrapper": "0.7.8", + "@novasamatech/host-api": "0.7.9", + "@novasamatech/host-api-wrapper": "0.7.9", "scale-ts": "1.6.1", "react-reconciler": "0.33.0" }, diff --git a/packages/scale/package.json b/packages/scale/package.json index 6a4340eb..c4ae6c7e 100644 --- a/packages/scale/package.json +++ b/packages/scale/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/scale", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "additional scale-ts bindings", "license": "Apache-2.0", "repository": { diff --git a/packages/statement-store/package.json b/packages/statement-store/package.json index 0df5d685..6e6fd46b 100644 --- a/packages/statement-store/package.json +++ b/packages/statement-store/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/statement-store", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Statement store integration", "license": "Apache-2.0", "repository": { @@ -25,9 +25,9 @@ "README.md" ], "dependencies": { - "@novasamatech/scale": "0.7.8", + "@novasamatech/scale": "0.7.9", "@novasamatech/sdk-statement": "^0.6.0", - "@polkadot-api/substrate-bindings": "^0.20.1", + "@polkadot-api/substrate-bindings": "^0.20.2", "@polkadot-api/substrate-client": "^0.7.0", "@polkadot-labs/hdkd-helpers": "^0.0.30", "@noble/hashes": "2.2.0", diff --git a/packages/storage-adapter/package.json b/packages/storage-adapter/package.json index 6e8e77b7..3985e626 100644 --- a/packages/storage-adapter/package.json +++ b/packages/storage-adapter/package.json @@ -1,7 +1,7 @@ { "name": "@novasamatech/storage-adapter", "type": "module", - "version": "0.7.8", + "version": "0.7.9", "description": "Statement store integration", "license": "Apache-2.0", "repository": {