diff --git a/core/signing-internal/src/controller.ts b/core/signing-internal/src/controller.ts index b5bf55f7c..3fff5814b 100644 --- a/core/signing-internal/src/controller.ts +++ b/core/signing-internal/src/controller.ts @@ -90,9 +90,9 @@ export class InternalSigningDriver implements SigningDriverInterface { hash: params.txHash, signature, publicKey: params.publicKey, - createdAt: new Date(), + createdAt: new Date().toISOString(), status: 'signed', - updatedAt: new Date(), + updatedAt: new Date().toISOString(), } this.store.setSigningTransaction( @@ -175,6 +175,7 @@ export class InternalSigningDriver implements SigningDriverInterface { convertInternalTransaction({ ...tx, signature: tx.signature || 'signed', + createdAt: new Date(tx.createdAt), }) ), }) @@ -230,8 +231,8 @@ export class InternalSigningDriver implements SigningDriverInterface { name: params.name, publicKey, privateKey, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), } await this.store.setSigningKey(_userId, internalKey) diff --git a/core/signing-lib/src/SigningDriverStore.ts b/core/signing-lib/src/SigningDriverStore.ts index 5c2dfdf6b..864a56f97 100644 --- a/core/signing-lib/src/SigningDriverStore.ts +++ b/core/signing-lib/src/SigningDriverStore.ts @@ -68,8 +68,8 @@ export interface SigningKey { publicKey: string privateKey?: string // Optional for external providers like Fireblocks metadata?: Record // Driver-specific data (e.g., derivation path) - createdAt: Date - updatedAt: Date + createdAt: string + updatedAt: string } /** @@ -82,8 +82,8 @@ export interface SigningTransaction { publicKey: string status: SigningDriverStatus metadata?: Record // Driver-specific data - createdAt: Date - updatedAt: Date + createdAt: string + updatedAt: string } /** diff --git a/core/signing-store-sql/package.json b/core/signing-store-sql/package.json index a33f4abf7..ace5936e9 100644 --- a/core/signing-store-sql/package.json +++ b/core/signing-store-sql/package.json @@ -33,6 +33,7 @@ "better-sqlite3": "^12.2.0", "commander": "^14.0.0", "kysely": "^0.28.5", + "pg": "^8.16.3", "pino": "^10.0.0", "umzug": "^3.8.2", "zod": "^3.25.64" @@ -40,6 +41,7 @@ "devDependencies": { "@swc/core": "^1.11.31", "@types/better-sqlite3": "^7.6.13", + "@types/pg": "^8", "tsup": "^8.5.0", "tsx": "^4.20.4", "typescript": "^5.8.3" diff --git a/core/signing-store-sql/src/migrations/001-init.ts b/core/signing-store-sql/src/migrations/001-init.ts index a8c7fceb8..a551d5eae 100644 --- a/core/signing-store-sql/src/migrations/001-init.ts +++ b/core/signing-store-sql/src/migrations/001-init.ts @@ -17,8 +17,8 @@ export async function up(db: Kysely): Promise { .addColumn('public_key', 'text', (col) => col.notNull()) .addColumn('private_key', 'text') // Encrypted for internal driver .addColumn('metadata', 'text') // JSON string for driver-specific data - .addColumn('created_at', 'integer', (col) => col.notNull()) - .addColumn('updated_at', 'integer', (col) => col.notNull()) + .addColumn('created_at', 'text', (col) => col.notNull()) + .addColumn('updated_at', 'text', (col) => col.notNull()) .addUniqueConstraint('signing_keys_user_id_id_unique', [ 'user_id', 'id', @@ -36,8 +36,8 @@ export async function up(db: Kysely): Promise { .addColumn('public_key', 'text', (col) => col.notNull()) .addColumn('status', 'text', (col) => col.notNull()) .addColumn('metadata', 'text') // JSON string for driver-specific data - .addColumn('created_at', 'integer', (col) => col.notNull()) - .addColumn('updated_at', 'integer', (col) => col.notNull()) + .addColumn('created_at', 'text', (col) => col.notNull()) + .addColumn('updated_at', 'text', (col) => col.notNull()) .addUniqueConstraint('signing_transactions_user_id_id_unique', [ 'user_id', 'id', diff --git a/core/signing-store-sql/src/schema.ts b/core/signing-store-sql/src/schema.ts index 3e20cb2d1..6801443a4 100644 --- a/core/signing-store-sql/src/schema.ts +++ b/core/signing-store-sql/src/schema.ts @@ -69,8 +69,8 @@ export const fromSigningKey = ( : key.privateKey : null, metadata: key.metadata ? JSON.stringify(key.metadata) : null, - createdAt: key.createdAt.toISOString(), - updatedAt: key.updatedAt.toISOString(), + createdAt: key.createdAt, + updatedAt: key.updatedAt, } } @@ -89,9 +89,16 @@ export const toSigningKey = ( : table.privateKey, } : {}), - createdAt: new Date(table.createdAt), - updatedAt: new Date(table.updatedAt), - ...(table.metadata ? { metadata: JSON.parse(table.metadata) } : {}), + createdAt: table.createdAt, + updatedAt: table.updatedAt, + ...(table.metadata + ? { + metadata: + typeof table.metadata === 'string' + ? JSON.parse(table.metadata) + : table.metadata, + } + : {}), } } @@ -109,8 +116,8 @@ export const fromSigningTransaction = ( metadata: transaction.metadata ? JSON.stringify(transaction.metadata) : null, - createdAt: transaction.createdAt.toISOString(), - updatedAt: transaction.updatedAt.toISOString(), + createdAt: transaction.createdAt, + updatedAt: transaction.updatedAt, } } @@ -123,9 +130,16 @@ export const toSigningTransaction = ( ...(table.signature ? { signature: table.signature } : {}), publicKey: table.publicKey, status: table.status as SigningDriverStatus, - ...(table.metadata ? { metadata: JSON.parse(table.metadata) } : {}), - createdAt: new Date(table.createdAt), - updatedAt: new Date(table.updatedAt), + ...(table.metadata + ? { + metadata: + typeof table.metadata === 'string' + ? JSON.parse(table.metadata) + : table.metadata, + } + : {}), + createdAt: table.createdAt, + updatedAt: table.updatedAt, } } @@ -145,7 +159,10 @@ export const toSigningDriverConfig = ( ): SigningDriverConfig => { return { driverId: table.driverId, - config: JSON.parse(table.config), + config: + typeof table.config === 'string' + ? JSON.parse(table.config) + : table.config, } } diff --git a/core/signing-store-sql/src/store-sql.ts b/core/signing-store-sql/src/store-sql.ts index 284a5003c..319ef3c8d 100644 --- a/core/signing-store-sql/src/store-sql.ts +++ b/core/signing-store-sql/src/store-sql.ts @@ -15,7 +15,8 @@ import { SigningDriverStatus, SigningDriverConfig, } from '@canton-network/core-signing-lib' -import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely' +import { CamelCasePlugin, Kysely, SqliteDialect, PostgresDialect } from 'kysely' +import { Pool } from 'pg' import Database from 'better-sqlite3' import { DB, @@ -191,11 +192,7 @@ export class StoreSql implements SigningDriverStore, AuthAware { if (before) { const beforeTx = await this.getSigningTransaction(userId, before) if (beforeTx) { - query = query.where( - 'createdAt', - '<', - beforeTx.createdAt.toISOString() - ) + query = query.where('createdAt', '<', beforeTx.createdAt) } } @@ -308,6 +305,19 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) + case 'postgres': + return new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + database: config.connection.database, + user: config.connection.user, + password: config.connection.password, + port: config.connection.port, + host: config.connection.host, + }), + }), + plugins: [new CamelCasePlugin()], + }) case 'memory': return new Kysely({ dialect: new SqliteDialect({ @@ -315,9 +325,5 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) - default: - throw new Error( - `Unsupported database type: ${config.connection.type}` - ) } } diff --git a/core/wallet-store-sql/package.json b/core/wallet-store-sql/package.json index 25bad1da1..8ee6e45cf 100644 --- a/core/wallet-store-sql/package.json +++ b/core/wallet-store-sql/package.json @@ -33,6 +33,7 @@ "better-sqlite3": "^12.2.0", "commander": "^14.0.0", "kysely": "^0.28.5", + "pg": "^8.16.3", "pino": "^10.0.0", "umzug": "^3.8.2", "zod": "^3.25.64" @@ -43,6 +44,7 @@ "@swc/jest": "^0.2.38", "@types/better-sqlite3": "^7.6.13", "@types/jest": "^30.0.0", + "@types/pg": "^8", "jest": "^30.0.0", "pino-test": "^1.1.0", "ts-jest": "^29.4.0", diff --git a/core/wallet-store-sql/src/schema.ts b/core/wallet-store-sql/src/schema.ts index 7be00e3d4..7ea90e438 100644 --- a/core/wallet-store-sql/src/schema.ts +++ b/core/wallet-store-sql/src/schema.ts @@ -122,9 +122,15 @@ export const toNetwork = (table: NetworkTable): Network => { ledgerApi: { baseUrl: table.ledgerApiBaseUrl, }, - auth: authSchema.parse(JSON.parse(table.auth)), + auth: authSchema.parse( + typeof table.auth === 'string' ? JSON.parse(table.auth) : table.auth + ), adminAuth: table.adminAuth - ? authSchema.parse(JSON.parse(table.adminAuth)) + ? authSchema.parse( + typeof table.adminAuth === 'string' + ? JSON.parse(table.adminAuth) + : table.adminAuth + ) : undefined, } } @@ -159,7 +165,7 @@ export const fromWallet = (wallet: Wallet, userId: UserId): WalletTable => { export const toWallet = (table: WalletTable): Wallet => { return { ...table, - primary: table.primary === 1, + primary: Boolean(table.primary), } } diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 1d8cb3f9e..d7786d37d 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -20,7 +20,7 @@ import { StoreConfig, UpdateWallet, } from '@canton-network/core-wallet-store' -import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely' +import { CamelCasePlugin, Kysely, PostgresDialect, SqliteDialect } from 'kysely' import Database from 'better-sqlite3' import { DB, @@ -33,6 +33,7 @@ import { toTransaction, toWallet, } from './schema.js' +import { Pool } from 'pg' export class StoreSql implements BaseStore, AuthAware { authContext: AuthContext | undefined @@ -440,6 +441,19 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) + case 'postgres': + return new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + database: config.connection.database, + user: config.connection.user, + password: config.connection.password, + port: config.connection.port, + host: config.connection.host, + }), + }), + plugins: [new CamelCasePlugin()], + }) case 'memory': return new Kysely({ dialect: new SqliteDialect({ @@ -447,9 +461,5 @@ export const connection = (config: StoreConfig) => { }), plugins: [new CamelCasePlugin()], }) - default: - throw new Error( - `Unsupported database type: ${config.connection.type}` - ) } } diff --git a/wallet-gateway/remote/README.md b/wallet-gateway/remote/README.md index 5ca8f150b..8be5f175d 100644 --- a/wallet-gateway/remote/README.md +++ b/wallet-gateway/remote/README.md @@ -41,3 +41,40 @@ The JSON-RPC API specs from `api-specs/` are generated into strongly-typed metho 2. Place the `fireblocks_secret.key` file at the path `/splice-wallet-kernel/wallet-gateway/remote` 3. Create a file named `fireblocks_api.key` at the path `/splice-wallet-kernel/wallet-gateway/remote` and insert your Fireblocks API key into it + +## Postgres connection + +To create a Postgres database you need to: + +1. Start Postgres in Docker using: + +```shell +$ docker run --network=host --name some-postgres -e POSTGRES_PASSWORD=postgres -d postgres +``` + +2. In the file `splice-wallet-kernel/wallet-gateway/test/config.json`, specify the connection settings for both databases (store and signingStore). The connection should look like this (it is important that `store.connection.database !== signingStore.connection.database !== 'postgres'`): + +```json +{ + "store": { + "connection": { + "type": "postgres", + "password": "postgres", + "port": 5432, + "user": "postgres", + "host": "0.0.0.0", + "database": "wallet_store" + } + }, + "signingStore": { + "connection": { + "type": "postgres", + "password": "postgres", + "port": 5432, + "user": "postgres", + "host": "0.0.0.0", + "database": "signing_store" + } + } +} +``` diff --git a/wallet-gateway/remote/package.json b/wallet-gateway/remote/package.json index 66c5f1e65..c1f1ebd48 100644 --- a/wallet-gateway/remote/package.json +++ b/wallet-gateway/remote/package.json @@ -53,6 +53,7 @@ "express": "^5.1.0", "express-rate-limit": "^7.5.1", "jose": "^5.10.0", + "kysely": "^0.28.5", "lit": "^3.3.0", "pino": "^9.7.0", "pino-pretty": "^13.0.0", diff --git a/wallet-gateway/remote/src/init.ts b/wallet-gateway/remote/src/init.ts index b1b8403aa..8f6213ba7 100644 --- a/wallet-gateway/remote/src/init.ts +++ b/wallet-gateway/remote/src/init.ts @@ -32,6 +32,7 @@ import { rpcRateLimit } from './middleware/rateLimit.js' import { Config } from './config/Config.js' import { existsSync, readFileSync } from 'fs' import path from 'path' +import { sql } from 'kysely' let isReady = false @@ -73,6 +74,28 @@ async function initializeDatabase( exists = existsSync(config.store.connection.database) } + if (config.store.connection.type === 'postgres') { + const db = connection({ + ...config.store, + connection: { ...config.store.connection, database: 'postgres' }, + }) + const result = await sql + .raw<{ + '?column?': number + }>(`select 1 from pg_database where datname='${config.store.connection.database}';`) + .execute(db) + const databaseExist = result.rows.length > 0 + if (!databaseExist) { + // Ignore error because postgres does not support `create database if nor exists` clause + await sql + .raw(`create database ${config.store.connection.database};`) + .execute(db) + .catch(() => {}) + exists = false + } + await db.destroy() + } + const db = connection(config.store) const umzug = migrator(db) const pending = await umzug.pending() @@ -108,6 +131,33 @@ async function initializeSigningDatabase( exists = existsSync(config.signingStore.connection.database) } + if (config.signingStore.connection.type === 'postgres') { + const db = signingConnection({ + ...config.signingStore, + connection: { + ...config.signingStore.connection, + database: 'postgres', + }, + }) + const result = await sql + .raw<{ + '?column?': number + }>(`select 1 from pg_database where datname='${config.signingStore.connection.database}';`) + .execute(db) + const databaseExist = result.rows.length > 0 + if (!databaseExist) { + // Ignore error because postgres does not support `create database if nor exists` clause + await sql + .raw( + `create database ${config.signingStore.connection.database};` + ) + .execute(db) + .catch(() => {}) + exists = false + } + await db.destroy() + } + const db = signingConnection(config.signingStore) const umzug = signingMigrator(db) const pending = await umzug.pending() diff --git a/yarn.lock b/yarn.lock index b9542dc86..342a579b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1605,9 +1605,11 @@ __metadata: "@canton-network/core-wallet-store": "workspace:^" "@swc/core": "npm:^1.11.31" "@types/better-sqlite3": "npm:^7.6.13" + "@types/pg": "npm:^8" better-sqlite3: "npm:^12.2.0" commander: "npm:^14.0.0" kysely: "npm:^0.28.5" + pg: "npm:^8.16.3" pino: "npm:^10.0.0" tsup: "npm:^8.5.0" tsx: "npm:^4.20.4" @@ -1794,10 +1796,12 @@ __metadata: "@swc/jest": "npm:^0.2.38" "@types/better-sqlite3": "npm:^7.6.13" "@types/jest": "npm:^30.0.0" + "@types/pg": "npm:^8" better-sqlite3: "npm:^12.2.0" commander: "npm:^14.0.0" jest: "npm:^30.0.0" kysely: "npm:^0.28.5" + pg: "npm:^8.16.3" pino: "npm:^10.0.0" pino-test: "npm:^1.1.0" ts-jest: "npm:^29.4.0" @@ -1998,6 +2002,7 @@ __metadata: express-rate-limit: "npm:^7.5.1" jest: "npm:^29.7.0" jose: "npm:^5.10.0" + kysely: "npm:^0.28.5" lit: "npm:^3.3.0" pino: "npm:^9.7.0" pino-pretty: "npm:^13.0.0" @@ -7549,6 +7554,17 @@ __metadata: languageName: node linkType: hard +"@types/pg@npm:^8": + version: 8.15.6 + resolution: "@types/pg@npm:8.15.6" + dependencies: + "@types/node": "npm:*" + pg-protocol: "npm:*" + pg-types: "npm:^2.2.0" + checksum: 10c0/7f93f83a4da0dc6133918f824d826fa34e78fb8cf86392d28a0e095c836c6910c014ced5d4b364d83e8485a65ce369adeb9663b14ba301241d4c0f80073007f3 + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.14.0 resolution: "@types/qs@npm:6.14.0" @@ -16697,6 +16713,87 @@ __metadata: languageName: node linkType: hard +"pg-cloudflare@npm:^1.2.7": + version: 1.2.7 + resolution: "pg-cloudflare@npm:1.2.7" + checksum: 10c0/8a52713dbdecc9d389dc4e65e3b7ede2e199ec3715f7491ee80a15db171f2d75677a102e9c2cef0cb91a2f310e91f976eaec0dd6ef5d8bf357de0b948f9d9431 + languageName: node + linkType: hard + +"pg-connection-string@npm:^2.9.1": + version: 2.9.1 + resolution: "pg-connection-string@npm:2.9.1" + checksum: 10c0/9a646529bbc0843806fc5de98ce93735a4612b571f11867178a85665d11989a827e6fd157388ca0e34ec948098564fce836c178cfd499b9f0e8cd9972b8e2e5c + languageName: node + linkType: hard + +"pg-int8@npm:1.0.1": + version: 1.0.1 + resolution: "pg-int8@npm:1.0.1" + checksum: 10c0/be6a02d851fc2a4ae3e9de81710d861de3ba35ac927268973eb3cb618873a05b9424656df464dd43bd7dc3fc5295c3f5b3c8349494f87c7af50ec59ef14e0b98 + languageName: node + linkType: hard + +"pg-pool@npm:^3.10.1": + version: 3.10.1 + resolution: "pg-pool@npm:3.10.1" + peerDependencies: + pg: ">=8.0" + checksum: 10c0/a00916b7df64226cc597fe769e3a757ff9b11562dc87ce5b0a54101a18c1fe282daaa2accaf27221e81e1e4cdf4da6a33dab09614734d32904d6c4e11c44a079 + languageName: node + linkType: hard + +"pg-protocol@npm:*, pg-protocol@npm:^1.10.3": + version: 1.10.3 + resolution: "pg-protocol@npm:1.10.3" + checksum: 10c0/f7ef54708c93ee6d271e37678296fc5097e4337fca91a88a3d99359b78633dbdbf6e983f0adb34b7cdd261b7ec7266deb20c3233bf3dfdb498b3e1098e8750b9 + languageName: node + linkType: hard + +"pg-types@npm:2.2.0, pg-types@npm:^2.2.0": + version: 2.2.0 + resolution: "pg-types@npm:2.2.0" + dependencies: + pg-int8: "npm:1.0.1" + postgres-array: "npm:~2.0.0" + postgres-bytea: "npm:~1.0.0" + postgres-date: "npm:~1.0.4" + postgres-interval: "npm:^1.1.0" + checksum: 10c0/ab3f8069a323f601cd2d2279ca8c425447dab3f9b61d933b0601d7ffc00d6200df25e26a4290b2b0783b59278198f7dd2ed03e94c4875797919605116a577c65 + languageName: node + linkType: hard + +"pg@npm:^8.16.3": + version: 8.16.3 + resolution: "pg@npm:8.16.3" + dependencies: + pg-cloudflare: "npm:^1.2.7" + pg-connection-string: "npm:^2.9.1" + pg-pool: "npm:^3.10.1" + pg-protocol: "npm:^1.10.3" + pg-types: "npm:2.2.0" + pgpass: "npm:1.0.5" + peerDependencies: + pg-native: ">=3.0.1" + dependenciesMeta: + pg-cloudflare: + optional: true + peerDependenciesMeta: + pg-native: + optional: true + checksum: 10c0/a6a407ff0efb7599760d72ffdcda47a74c34c0fd71d896623caac45cf2cfb0f49a10973cce23110f182b9810639a1e9f6904454d7358c7001574ee0ffdcbce2a + languageName: node + linkType: hard + +"pgpass@npm:1.0.5": + version: 1.0.5 + resolution: "pgpass@npm:1.0.5" + dependencies: + split2: "npm:^4.1.0" + checksum: 10c0/5ea6c9b2de04c33abb08d33a2dded303c4a3c7162a9264519cbe85c0a9857d712463140ba42fad0c7cd4b21f644dd870b45bb2e02fcbe505b4de0744fd802c1d + languageName: node + linkType: hard + "picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -17089,6 +17186,36 @@ __metadata: languageName: node linkType: hard +"postgres-array@npm:~2.0.0": + version: 2.0.0 + resolution: "postgres-array@npm:2.0.0" + checksum: 10c0/cbd56207e4141d7fbf08c86f2aebf21fa7064943d3f808ec85f442ff94b48d891e7a144cc02665fb2de5dbcb9b8e3183a2ac749959e794b4a4cfd379d7a21d08 + languageName: node + linkType: hard + +"postgres-bytea@npm:~1.0.0": + version: 1.0.0 + resolution: "postgres-bytea@npm:1.0.0" + checksum: 10c0/febf2364b8a8953695cac159eeb94542ead5886792a9627b97e33f6b5bb6e263bc0706ab47ec221516e79fbd6b2452d668841830fb3b49ec6c0fc29be61892ce + languageName: node + linkType: hard + +"postgres-date@npm:~1.0.4": + version: 1.0.7 + resolution: "postgres-date@npm:1.0.7" + checksum: 10c0/0ff91fccc64003e10b767fcfeefb5eaffbc522c93aa65d5051c49b3c4ce6cb93ab091a7d22877a90ad60b8874202c6f1d0f935f38a7235ed3b258efd54b97ca9 + languageName: node + linkType: hard + +"postgres-interval@npm:^1.1.0": + version: 1.2.0 + resolution: "postgres-interval@npm:1.2.0" + dependencies: + xtend: "npm:^4.0.0" + checksum: 10c0/c1734c3cb79e7f22579af0b268a463b1fa1d084e742a02a7a290c4f041e349456f3bee3b4ee0bb3f226828597f7b76deb615c1b857db9a742c45520100456272 + languageName: node + linkType: hard + "prebuild-install@npm:^7.1.1": version: 7.1.3 resolution: "prebuild-install@npm:7.1.3" @@ -18610,7 +18737,7 @@ __metadata: languageName: unknown linkType: soft -"split2@npm:^4.0.0, split2@npm:^4.2.0": +"split2@npm:^4.0.0, split2@npm:^4.1.0, split2@npm:^4.2.0": version: 4.2.0 resolution: "split2@npm:4.2.0" checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 @@ -20694,6 +20821,13 @@ __metadata: languageName: node linkType: hard +"xtend@npm:^4.0.0": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8"