diff --git a/.changeset/orange-cars-worry.md b/.changeset/orange-cars-worry.md new file mode 100644 index 000000000..d2c5b90c4 --- /dev/null +++ b/.changeset/orange-cars-worry.md @@ -0,0 +1,5 @@ +--- +"@frak-labs/nexus-sdk": patch +--- + +Add a `modalBuilder` to ease modal creation diff --git a/example/vanilla-js/src/main.ts b/example/vanilla-js/src/main.ts index 6b9b0d3fb..3024997ec 100644 --- a/example/vanilla-js/src/main.ts +++ b/example/vanilla-js/src/main.ts @@ -5,7 +5,12 @@ import { setupFrakClient } from "./module/setupClient"; import { displayWalletStatus } from "./module/walletStatus"; // Export the setup function and config for use in other files -window.FrakSetup = { frakConfig, frakClient: null, modalShare }; +window.FrakSetup = { + frakConfig, + frakClient: null, + modalShare, + modalBuilder: null, +}; document.addEventListener("DOMContentLoaded", () => { console.log("NexusSDK", window.NexusSDK); @@ -16,22 +21,19 @@ document.addEventListener("DOMContentLoaded", () => { return; } + const modalStepBuilder = window.NexusSDK.modalBuilder(frakClient, { + metadata: { + lang: "fr", + isDismissible: true, + }, + login: loginModalStep, + }); + window.FrakSetup.frakClient = frakClient; + window.FrakSetup.modalBuilder = modalStepBuilder; window.NexusSDK.referralInteraction(frakClient, { - modalConfig: { - steps: { - login: loginModalStep, - openSession: {}, - final: { - action: { key: "reward" }, - }, - }, - metadata: { - lang: "fr", - isDismissible: true, - }, - }, + modalConfig: modalStepBuilder.reward().params, options: { alwaysAppendUrl: true, }, diff --git a/example/vanilla-js/src/module/login.ts b/example/vanilla-js/src/module/login.ts index 2cd412b56..528ac852d 100644 --- a/example/vanilla-js/src/module/login.ts +++ b/example/vanilla-js/src/module/login.ts @@ -1,5 +1,3 @@ -import { loginModalStep } from "./config"; - export function bindLoginButton() { const loginButton = document.getElementById("login-button"); loginButton?.addEventListener("click", handleLogin); @@ -19,20 +17,11 @@ async function handleLogin() { loginButton.textContent = "Logging in..."; try { - if (!window.FrakSetup.frakClient) { + if (!window.FrakSetup.modalBuilder) { console.error("Frak client not initialized"); return; } - await window.NexusSDK.displayModal(window.FrakSetup.frakClient, { - metadata: { - lang: "fr", - isDismissible: true, - }, - steps: { - login: loginModalStep, - openSession: {}, - }, - }); + await window.FrakSetup.modalBuilder.display(); loginButton.textContent = "Logged In"; } catch (error) { console.error("Login error:", error); diff --git a/example/vanilla-js/src/module/modalShare.ts b/example/vanilla-js/src/module/modalShare.ts index e10d7d07f..a052cb6f0 100644 --- a/example/vanilla-js/src/module/modalShare.ts +++ b/example/vanilla-js/src/module/modalShare.ts @@ -1,29 +1,13 @@ -import { loginModalStep } from "./config"; - export function modalShare() { - const finalAction = { - key: "sharing", - options: { - popupTitle: "Share this article with your friends", - text: "Discover this awesome article", - link: typeof window !== "undefined" ? window.location.href : "", - }, - } as const; - if (!window.FrakSetup.frakClient) { + if (!window.FrakSetup.modalBuilder) { console.error("Frak client not initialized"); return; } - window.NexusSDK.displayModal(window.FrakSetup.frakClient, { - metadata: { - lang: "fr", - isDismissible: true, - }, - steps: { - login: loginModalStep, - openSession: {}, - final: { - action: finalAction, - }, - }, - }); + window.FrakSetup.modalBuilder + .sharing({ + popupTitle: "Share this article with your friends", + text: "Discover this awesome article", + link: typeof window !== "undefined" ? window.location.href : "", + }) + .display(); } diff --git a/example/vanilla-js/src/types/globals.d.ts b/example/vanilla-js/src/types/globals.d.ts index 223bb351a..949b71050 100644 --- a/example/vanilla-js/src/types/globals.d.ts +++ b/example/vanilla-js/src/types/globals.d.ts @@ -1,5 +1,7 @@ import type { + ModalBuilder, displayModal, + modalBuilder, referralInteraction, watchWalletStatus, } from "@frak-labs/nexus-sdk/actions"; @@ -18,11 +20,13 @@ declare global { displayModal: typeof displayModal; referralInteraction: typeof referralInteraction; watchWalletStatus: typeof watchWalletStatus; + modalBuilder: typeof modalBuilder; }; FrakSetup: { frakConfig: NexusWalletSdkConfig; frakClient: NexusClient | null; modalShare: () => void; + modalBuilder: ModalBuilder | null; }; } } diff --git a/packages/backend-elysia/drizzle/dev/0005_bumpy_odin.sql b/packages/backend-elysia/drizzle/dev/0005_bumpy_odin.sql new file mode 100644 index 000000000..173f56f20 --- /dev/null +++ b/packages/backend-elysia/drizzle/dev/0005_bumpy_odin.sql @@ -0,0 +1,10 @@ +-- Drop items that would conflict with the created index +DELETE FROM "product_oracle_purchase_item" +WHERE id NOT IN ( + SELECT MIN(id) + FROM "product_oracle_purchase_item" + GROUP BY "external_id", "purchase_id" +); + +-- Create the index +CREATE UNIQUE INDEX IF NOT EXISTS "unique_external_purchase_item_id" ON "product_oracle_purchase_item" USING btree ("external_id","purchase_id"); \ No newline at end of file diff --git a/packages/backend-elysia/drizzle/dev/0006_moaning_mastermind.sql b/packages/backend-elysia/drizzle/dev/0006_moaning_mastermind.sql new file mode 100644 index 000000000..86d913e67 --- /dev/null +++ b/packages/backend-elysia/drizzle/dev/0006_moaning_mastermind.sql @@ -0,0 +1,8 @@ +DO $$ BEGIN + CREATE TYPE "public"."product_oracle_plateform" AS ENUM('shopify', 'woocommerce', 'custom'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +ALTER TABLE "product_oracle" ADD COLUMN "plateform" "product_oracle_plateform" DEFAULT 'shopify' NOT NULL;--> statement-breakpoint +ALTER TABLE "product_oracle_purchase_item" ADD COLUMN "image_url" varchar; \ No newline at end of file diff --git a/packages/backend-elysia/drizzle/dev/0007_flashy_shinko_yamashiro.sql b/packages/backend-elysia/drizzle/dev/0007_flashy_shinko_yamashiro.sql new file mode 100644 index 000000000..5a96b3569 --- /dev/null +++ b/packages/backend-elysia/drizzle/dev/0007_flashy_shinko_yamashiro.sql @@ -0,0 +1 @@ +ALTER TABLE "product_oracle" RENAME COLUMN "plateform" TO "platform"; \ No newline at end of file diff --git a/packages/backend-elysia/drizzle/dev/meta/0005_snapshot.json b/packages/backend-elysia/drizzle/dev/meta/0005_snapshot.json new file mode 100644 index 000000000..dff0db58c --- /dev/null +++ b/packages/backend-elysia/drizzle/dev/meta/0005_snapshot.json @@ -0,0 +1,802 @@ +{ + "id": "1b47cbcd-6d4e-4182-8ca5-f07fffe8c31e", + "prevId": "0c1813b9-1fd3-4fba-8c93-f47b3e8a8318", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sso_session": { + "name": "sso_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "sso_id": { + "name": "sso_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "consume_key": { + "name": "consume_key", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "authenticator_id": { + "name": "authenticator_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_idx": { + "name": "sso_idx", + "columns": [ + { + "expression": "sso_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_product_idx": { + "name": "sso_product_idx", + "columns": [ + { + "expression": "sso_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.interactions_purchase_tracker": { + "name": "interactions_purchase_tracker", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_purchase_id": { + "name": "external_purchase_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "external_customer_id": { + "name": "external_customer_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pushed": { + "name": "pushed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_interactions_purchase_map_idx": { + "name": "wallet_interactions_purchase_map_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_map_idx": { + "name": "unique_map_idx", + "nullsNotDistinct": false, + "columns": ["external_purchase_id", "external_customer_id"] + } + } + }, + "public.interactions_pending": { + "name": "interactions_pending", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "type_denominator": { + "name": "type_denominator", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "interaction_data": { + "name": "interaction_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "simulation_status": { + "name": "simulation_status", + "type": "interactions_simulation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "wallet_pending_interactions_idx": { + "name": "wallet_pending_interactions_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "product_idx": { + "name": "product_idx", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.interactions_pushed": { + "name": "interactions_pushed", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "type_denominator": { + "name": "type_denominator", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "interaction_data": { + "name": "interaction_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_pushed_interactions_idx": { + "name": "wallet_pushed_interactions_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.push_tokens": { + "name": "push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key_p256dh": { + "name": "key_p256dh", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key_auth": { + "name": "key_auth", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "expire_at": { + "name": "expire_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_push_tokens_idx": { + "name": "wallet_push_tokens_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_push_token": { + "name": "unique_push_token", + "nullsNotDistinct": false, + "columns": ["wallet", "endpoint", "key_p256dh"] + } + } + }, + "public.product_oracle": { + "name": "product_oracle", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "hook_signature_key": { + "name": "hook_signature_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "merkle_root": { + "name": "merkle_root", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_sync_tx_hash": { + "name": "last_sync_tx_hash", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_product_id": { + "name": "unique_product_id", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_oracle_product_id_unique": { + "name": "product_oracle_product_id_unique", + "nullsNotDistinct": false, + "columns": ["product_id"] + } + } + }, + "public.product_oracle_purchase_item": { + "name": "product_oracle_purchase_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "purchase_id": { + "name": "purchase_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "item_purchase_id_idx": { + "name": "item_purchase_id_idx", + "columns": [ + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_external_purchase_item_id": { + "name": "unique_external_purchase_item_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_oracle_purchase_item_purchase_id_product_oracle_purchase_purchase_id_fk": { + "name": "product_oracle_purchase_item_purchase_id_product_oracle_purchase_purchase_id_fk", + "tableFrom": "product_oracle_purchase_item", + "tableTo": "product_oracle_purchase", + "columnsFrom": ["purchase_id"], + "columnsTo": ["purchase_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.product_oracle_purchase": { + "name": "product_oracle_purchase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "oracle_id": { + "name": "oracle_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purchase_id": { + "name": "purchase_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "external_customer_id": { + "name": "external_customer_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "purchase_token": { + "name": "purchase_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "total_price": { + "name": "total_price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency_code": { + "name": "currency_code", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "purchase_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "leaf": { + "name": "leaf", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_external_id": { + "name": "unique_external_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "oracle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_id_idx": { + "name": "purchase_id_idx", + "columns": [ + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_listener_id": { + "name": "external_listener_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchase_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_oracle_purchase_oracle_id_product_oracle_id_fk": { + "name": "product_oracle_purchase_oracle_id_product_oracle_id_fk", + "tableFrom": "product_oracle_purchase", + "tableTo": "product_oracle", + "columnsFrom": ["oracle_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_oracle_purchase_purchase_id_unique": { + "name": "product_oracle_purchase_purchase_id_unique", + "nullsNotDistinct": false, + "columns": ["purchase_id"] + } + } + } + }, + "enums": { + "public.interactions_simulation_status": { + "name": "interactions_simulation_status", + "schema": "public", + "values": ["pending", "no_session", "failed", "succeeded"] + }, + "public.purchase_status": { + "name": "purchase_status", + "schema": "public", + "values": ["pending", "confirmed", "cancelled", "refunded"] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/backend-elysia/drizzle/dev/meta/0006_snapshot.json b/packages/backend-elysia/drizzle/dev/meta/0006_snapshot.json new file mode 100644 index 000000000..f32e06186 --- /dev/null +++ b/packages/backend-elysia/drizzle/dev/meta/0006_snapshot.json @@ -0,0 +1,821 @@ +{ + "id": "fcea6915-8fc5-490b-9938-51fc7492dbe8", + "prevId": "1b47cbcd-6d4e-4182-8ca5-f07fffe8c31e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sso_session": { + "name": "sso_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "sso_id": { + "name": "sso_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "consume_key": { + "name": "consume_key", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "authenticator_id": { + "name": "authenticator_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_idx": { + "name": "sso_idx", + "columns": [ + { + "expression": "sso_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_product_idx": { + "name": "sso_product_idx", + "columns": [ + { + "expression": "sso_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.interactions_purchase_tracker": { + "name": "interactions_purchase_tracker", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_purchase_id": { + "name": "external_purchase_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "external_customer_id": { + "name": "external_customer_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pushed": { + "name": "pushed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_interactions_purchase_map_idx": { + "name": "wallet_interactions_purchase_map_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_map_idx": { + "name": "unique_map_idx", + "nullsNotDistinct": false, + "columns": ["external_purchase_id", "external_customer_id"] + } + } + }, + "public.interactions_pending": { + "name": "interactions_pending", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "type_denominator": { + "name": "type_denominator", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "interaction_data": { + "name": "interaction_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "simulation_status": { + "name": "simulation_status", + "type": "interactions_simulation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "wallet_pending_interactions_idx": { + "name": "wallet_pending_interactions_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "product_idx": { + "name": "product_idx", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.interactions_pushed": { + "name": "interactions_pushed", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "type_denominator": { + "name": "type_denominator", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "interaction_data": { + "name": "interaction_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_pushed_interactions_idx": { + "name": "wallet_pushed_interactions_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.push_tokens": { + "name": "push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key_p256dh": { + "name": "key_p256dh", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key_auth": { + "name": "key_auth", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "expire_at": { + "name": "expire_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_push_tokens_idx": { + "name": "wallet_push_tokens_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_push_token": { + "name": "unique_push_token", + "nullsNotDistinct": false, + "columns": ["wallet", "endpoint", "key_p256dh"] + } + } + }, + "public.product_oracle": { + "name": "product_oracle", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "hook_signature_key": { + "name": "hook_signature_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "plateform": { + "name": "plateform", + "type": "product_oracle_plateform", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'shopify'" + }, + "merkle_root": { + "name": "merkle_root", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_sync_tx_hash": { + "name": "last_sync_tx_hash", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_product_id": { + "name": "unique_product_id", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_oracle_product_id_unique": { + "name": "product_oracle_product_id_unique", + "nullsNotDistinct": false, + "columns": ["product_id"] + } + } + }, + "public.product_oracle_purchase_item": { + "name": "product_oracle_purchase_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "purchase_id": { + "name": "purchase_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "item_purchase_id_idx": { + "name": "item_purchase_id_idx", + "columns": [ + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_external_purchase_item_id": { + "name": "unique_external_purchase_item_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_oracle_purchase_item_purchase_id_product_oracle_purchase_purchase_id_fk": { + "name": "product_oracle_purchase_item_purchase_id_product_oracle_purchase_purchase_id_fk", + "tableFrom": "product_oracle_purchase_item", + "tableTo": "product_oracle_purchase", + "columnsFrom": ["purchase_id"], + "columnsTo": ["purchase_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.product_oracle_purchase": { + "name": "product_oracle_purchase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "oracle_id": { + "name": "oracle_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purchase_id": { + "name": "purchase_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "external_customer_id": { + "name": "external_customer_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "purchase_token": { + "name": "purchase_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "total_price": { + "name": "total_price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency_code": { + "name": "currency_code", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "purchase_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "leaf": { + "name": "leaf", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_external_id": { + "name": "unique_external_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "oracle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_id_idx": { + "name": "purchase_id_idx", + "columns": [ + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_listener_id": { + "name": "external_listener_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchase_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_oracle_purchase_oracle_id_product_oracle_id_fk": { + "name": "product_oracle_purchase_oracle_id_product_oracle_id_fk", + "tableFrom": "product_oracle_purchase", + "tableTo": "product_oracle", + "columnsFrom": ["oracle_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_oracle_purchase_purchase_id_unique": { + "name": "product_oracle_purchase_purchase_id_unique", + "nullsNotDistinct": false, + "columns": ["purchase_id"] + } + } + } + }, + "enums": { + "public.interactions_simulation_status": { + "name": "interactions_simulation_status", + "schema": "public", + "values": ["pending", "no_session", "failed", "succeeded"] + }, + "public.product_oracle_plateform": { + "name": "product_oracle_plateform", + "schema": "public", + "values": ["shopify", "woocommerce", "custom"] + }, + "public.purchase_status": { + "name": "purchase_status", + "schema": "public", + "values": ["pending", "confirmed", "cancelled", "refunded"] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/backend-elysia/drizzle/dev/meta/0007_snapshot.json b/packages/backend-elysia/drizzle/dev/meta/0007_snapshot.json new file mode 100644 index 000000000..d8bc79286 --- /dev/null +++ b/packages/backend-elysia/drizzle/dev/meta/0007_snapshot.json @@ -0,0 +1,821 @@ +{ + "id": "918f013e-1ebd-4009-b637-e9e7492a4a5e", + "prevId": "fcea6915-8fc5-490b-9938-51fc7492dbe8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sso_session": { + "name": "sso_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "sso_id": { + "name": "sso_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "consume_key": { + "name": "consume_key", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "authenticator_id": { + "name": "authenticator_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_idx": { + "name": "sso_idx", + "columns": [ + { + "expression": "sso_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_product_idx": { + "name": "sso_product_idx", + "columns": [ + { + "expression": "sso_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.interactions_purchase_tracker": { + "name": "interactions_purchase_tracker", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_purchase_id": { + "name": "external_purchase_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "external_customer_id": { + "name": "external_customer_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pushed": { + "name": "pushed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_interactions_purchase_map_idx": { + "name": "wallet_interactions_purchase_map_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_map_idx": { + "name": "unique_map_idx", + "nullsNotDistinct": false, + "columns": ["external_purchase_id", "external_customer_id"] + } + } + }, + "public.interactions_pending": { + "name": "interactions_pending", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "type_denominator": { + "name": "type_denominator", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "interaction_data": { + "name": "interaction_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "simulation_status": { + "name": "simulation_status", + "type": "interactions_simulation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "wallet_pending_interactions_idx": { + "name": "wallet_pending_interactions_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "product_idx": { + "name": "product_idx", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.interactions_pushed": { + "name": "interactions_pushed", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "type_denominator": { + "name": "type_denominator", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "interaction_data": { + "name": "interaction_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_pushed_interactions_idx": { + "name": "wallet_pushed_interactions_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.push_tokens": { + "name": "push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key_p256dh": { + "name": "key_p256dh", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key_auth": { + "name": "key_auth", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "expire_at": { + "name": "expire_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "wallet_push_tokens_idx": { + "name": "wallet_push_tokens_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_push_token": { + "name": "unique_push_token", + "nullsNotDistinct": false, + "columns": ["wallet", "endpoint", "key_p256dh"] + } + } + }, + "public.product_oracle": { + "name": "product_oracle", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "hook_signature_key": { + "name": "hook_signature_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "platform": { + "name": "platform", + "type": "product_oracle_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'shopify'" + }, + "merkle_root": { + "name": "merkle_root", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_sync_tx_hash": { + "name": "last_sync_tx_hash", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_product_id": { + "name": "unique_product_id", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_oracle_product_id_unique": { + "name": "product_oracle_product_id_unique", + "nullsNotDistinct": false, + "columns": ["product_id"] + } + } + }, + "public.product_oracle_purchase_item": { + "name": "product_oracle_purchase_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "purchase_id": { + "name": "purchase_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "item_purchase_id_idx": { + "name": "item_purchase_id_idx", + "columns": [ + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_external_purchase_item_id": { + "name": "unique_external_purchase_item_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_oracle_purchase_item_purchase_id_product_oracle_purchase_purchase_id_fk": { + "name": "product_oracle_purchase_item_purchase_id_product_oracle_purchase_purchase_id_fk", + "tableFrom": "product_oracle_purchase_item", + "tableTo": "product_oracle_purchase", + "columnsFrom": ["purchase_id"], + "columnsTo": ["purchase_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.product_oracle_purchase": { + "name": "product_oracle_purchase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "oracle_id": { + "name": "oracle_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purchase_id": { + "name": "purchase_id", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "external_customer_id": { + "name": "external_customer_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "purchase_token": { + "name": "purchase_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "total_price": { + "name": "total_price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency_code": { + "name": "currency_code", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "purchase_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "leaf": { + "name": "leaf", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_external_id": { + "name": "unique_external_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "oracle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_id_idx": { + "name": "purchase_id_idx", + "columns": [ + { + "expression": "purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_listener_id": { + "name": "external_listener_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchase_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_oracle_purchase_oracle_id_product_oracle_id_fk": { + "name": "product_oracle_purchase_oracle_id_product_oracle_id_fk", + "tableFrom": "product_oracle_purchase", + "tableTo": "product_oracle", + "columnsFrom": ["oracle_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_oracle_purchase_purchase_id_unique": { + "name": "product_oracle_purchase_purchase_id_unique", + "nullsNotDistinct": false, + "columns": ["purchase_id"] + } + } + } + }, + "enums": { + "public.interactions_simulation_status": { + "name": "interactions_simulation_status", + "schema": "public", + "values": ["pending", "no_session", "failed", "succeeded"] + }, + "public.product_oracle_platform": { + "name": "product_oracle_platform", + "schema": "public", + "values": ["shopify", "woocommerce", "custom"] + }, + "public.purchase_status": { + "name": "purchase_status", + "schema": "public", + "values": ["pending", "confirmed", "cancelled", "refunded"] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/backend-elysia/drizzle/dev/meta/_journal.json b/packages/backend-elysia/drizzle/dev/meta/_journal.json index f7a22f6dc..92b275cd9 100644 --- a/packages/backend-elysia/drizzle/dev/meta/_journal.json +++ b/packages/backend-elysia/drizzle/dev/meta/_journal.json @@ -36,6 +36,27 @@ "when": 1730116392056, "tag": "0004_tired_mephisto", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1730234900586, + "tag": "0005_bumpy_odin", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1730286897092, + "tag": "0006_moaning_mastermind", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1730287205572, + "tag": "0007_flashy_shinko_yamashiro", + "breakpoints": true } ] } diff --git a/packages/backend-elysia/src/domain/business/repositories/MintRepository.ts b/packages/backend-elysia/src/domain/business/repositories/MintRepository.ts index 418c819c5..08beb89ca 100644 --- a/packages/backend-elysia/src/domain/business/repositories/MintRepository.ts +++ b/packages/backend-elysia/src/domain/business/repositories/MintRepository.ts @@ -19,6 +19,7 @@ import { type Address, type Chain, type Client, + type Hex, type LocalAccount, type Transport, isAddressEqual, @@ -136,21 +137,22 @@ export class MintRepository { }); // Deploy the matching interaction contract - await this.deployInteractionContract({ + const interactionResult = await this.deployInteractionContract({ productId: precomputedProductId, minter, lock, }); // Then deploy a mocked usd bank for this product + let bankResult: { txHash: Hex; bank: Address } | undefined; if (isRunningInProd) { - await this.deployUsdcBank({ + bankResult = await this.deployUsdcBank({ productId: precomputedProductId, minter, lock, }); } else { - await this.deployMockedUsdBank({ + bankResult = await this.deployMockedUsdBank({ productId: precomputedProductId, minter, lock, @@ -160,6 +162,8 @@ export class MintRepository { return { productId: precomputedProductId, mintTxHash, + interactionResult, + bankResult, }; } @@ -176,7 +180,7 @@ export class MintRepository { lock, }: { productId: bigint; minter: LocalAccount; lock: Mutex }) { try { - const hash = await lock.runExclusive(async () => { + const result = await lock.runExclusive(async () => { // Prepare the deployment data const { request, result } = await simulateContract( this.client, @@ -193,14 +197,17 @@ export class MintRepository { } // Trigger the deployment - return await writeContract(this.client, request); + const txHash = await writeContract(this.client, request); + return { txHash, interactionContract: result }; }); - if (!hash) return; + if (!result) return; // Ensure it's included before proceeding await waitForTransactionReceipt(this.client, { - hash, + hash: result.txHash, confirmations: 1, }); + // And return everything + return result; } catch (e) { log.warn( { productId, error: e }, @@ -221,7 +228,7 @@ export class MintRepository { lock, }: { productId: bigint; minter: LocalAccount; lock: Mutex }) { try { - await lock.runExclusive(async () => { + return await lock.runExclusive(async () => { // Get the current nonce const nonce = await getTransactionCount(this.client, minter); @@ -242,7 +249,7 @@ export class MintRepository { } // Trigger the deployment - await writeContract(this.client, request); + const txHash = await writeContract(this.client, request); // Then mint a few test tokens to this bank await writeContract(this.client, { @@ -253,6 +260,9 @@ export class MintRepository { args: [result, parseEther("500")], nonce: nonce + 1, }); + + // Then return the hash + contract + return { txHash, bank: result }; }); } catch (e) { log.warn( @@ -266,6 +276,7 @@ export class MintRepository { * Automatically deploy a mocked usd bank for the given product * @param productId * @param minter + * @param lock * @private */ private async deployUsdcBank({ @@ -274,7 +285,7 @@ export class MintRepository { lock, }: { productId: bigint; minter: LocalAccount; lock: Mutex }) { try { - await lock.runExclusive(async () => { + return await lock.runExclusive(async () => { // Prepare the deployment data const { request, result } = await simulateContract( this.client, @@ -291,7 +302,10 @@ export class MintRepository { } // Trigger the deployment - await writeContract(this.client, request); + const txHash = await writeContract(this.client, request); + + // Then return the hash + contract + return { txHash, bank: result }; }); } catch (e) { log.warn({ productId, error: e }, "Failed to deploy the usdc bank"); diff --git a/packages/backend-elysia/src/domain/business/routes/mint.ts b/packages/backend-elysia/src/domain/business/routes/mint.ts index 3b241ba12..5aaedbf72 100644 --- a/packages/backend-elysia/src/domain/business/routes/mint.ts +++ b/packages/backend-elysia/src/domain/business/routes/mint.ts @@ -113,7 +113,7 @@ export const mintRoutes = new Elysia({ prefix: "/mint" }) // Mint a product try { - const { mintTxHash, productId } = + const { mintTxHash, productId, bankResult, interactionResult } = await mintRepository.mintProduct({ name, domain: normalisedDomain, @@ -121,7 +121,12 @@ export const mintRoutes = new Elysia({ prefix: "/mint" }) owner: businessSession.wallet, }); - return { txHash: mintTxHash, productId: toHex(productId) }; + return { + txHash: mintTxHash, + productId: toHex(productId), + interactionContract: interactionResult?.interactionContract, + bankContract: bankResult?.bank, + }; } catch (e) { return error(400, (e as Error)?.message ?? "An error occurred"); } @@ -147,6 +152,8 @@ export const mintRoutes = new Elysia({ prefix: "/mint" }) 200: t.Object({ txHash: t.Hex(), productId: t.Hex(), + interactionContract: t.Optional(t.Hex()), + bankContract: t.Optional(t.Hex()), }), }, } diff --git a/packages/backend-elysia/src/domain/interactions/jobs/purchaseTracker.ts b/packages/backend-elysia/src/domain/interactions/jobs/purchaseTracker.ts index 5f9bdaf74..f4e8dedef 100644 --- a/packages/backend-elysia/src/domain/interactions/jobs/purchaseTracker.ts +++ b/packages/backend-elysia/src/domain/interactions/jobs/purchaseTracker.ts @@ -51,6 +51,13 @@ const innerPurchaseTrackerJob = (app: OuterPurchaseTrackerApp) => ); continue; } + if (result.purchase.status !== "confirmed") { + log.debug( + { result, tracker }, + "Purchase not completed yet for tracker" + ); + continue; + } // If all good, build the interaction and push it const interaction = diff --git a/packages/backend-elysia/src/domain/interactions/routes/purchase.ts b/packages/backend-elysia/src/domain/interactions/routes/purchase.ts index 1bdea9d11..64736f79b 100644 --- a/packages/backend-elysia/src/domain/interactions/routes/purchase.ts +++ b/packages/backend-elysia/src/domain/interactions/routes/purchase.ts @@ -1,34 +1,59 @@ -import { log, walletSdkSessionContext } from "@backend-common"; +import { log, sessionContext } from "@backend-common"; import { t } from "@backend-utils"; import { Elysia } from "elysia"; +import { type Address, isHex } from "viem"; import { interactionsContext } from "../context"; import { interactionsPurchaseTrackerTable } from "../db/schema"; export const purchaseInteractionsRoutes = new Elysia() .use(interactionsContext) - .use(walletSdkSessionContext) + .use(sessionContext) .post( "/listenForPurchase", - async ({ body, interactionsDb, walletSdkSession }) => { - if (!walletSdkSession) return; + async ({ + body, + headers: { "x-wallet-sdk-auth": walletSdkAuth }, + interactionsDb, + walletSdkJwt, + error, + }) => { + if (!walletSdkAuth) return error(401, "Missing wallet SDK JWT"); - log.debug(`Received purchase from ${body.customerId}`); + // Get the right address + let address: Address; + if (isHex(walletSdkAuth)) { + // Condition required for initial implementation, should be updated in a later stage to enforce wallet session + address = walletSdkAuth; + } else { + const session = await walletSdkJwt.verify(walletSdkAuth); + if (!session) return error(401, "Invalid wallet SDK JWT"); + address = session.address; + } + + log.debug(`Received purchase from ${body.customerId} - ${address}`); // Insert the purchase tracker await interactionsDb .insert(interactionsPurchaseTrackerTable) .values({ - wallet: walletSdkSession.address, - externalCustomerId: body.customerId, - externalPurchaseId: body.orderId, + wallet: address, + externalCustomerId: + typeof body.customerId !== "string" + ? body.customerId.toString() + : body.customerId, + externalPurchaseId: + typeof body.orderId !== "string" + ? body.orderId.toString() + : body.orderId, token: body.token, - }); + }) + .onConflictDoNothing(); }, { - authenticated: "wallet-sdk", + type: "json", body: t.Object({ - customerId: t.String(), - orderId: t.String(), + customerId: t.Union([t.String(), t.Number()]), + orderId: t.Union([t.String(), t.Number()]), token: t.String(), }), } diff --git a/packages/backend-elysia/src/domain/oracle/db/schema.ts b/packages/backend-elysia/src/domain/oracle/db/schema.ts index e26272ff5..b21582288 100644 --- a/packages/backend-elysia/src/domain/oracle/db/schema.ts +++ b/packages/backend-elysia/src/domain/oracle/db/schema.ts @@ -12,6 +12,12 @@ import { } from "drizzle-orm/pg-core"; import { customHex } from "../../../utils/drizzle/customTypes"; +export const productOraclePlatformEnum = pgEnum("product_oracle_platform", [ + "shopify", + "woocommerce", + "custom", +]); + export const productOracleTable = pgTable( "product_oracle", { @@ -21,6 +27,10 @@ export const productOracleTable = pgTable( hookSignatureKey: varchar("hook_signature_key").notNull(), // Date infos createdAt: timestamp("created_at").defaultNow(), + // The plateform of the oracle + platform: productOraclePlatformEnum("platform") + .notNull() + .default("shopify"), // The current merkle root for this oracle merkleRoot: customHex("merkle_root"), // If the oracle is synced with the blockchain @@ -94,6 +104,8 @@ export const purchaseItemTable = pgTable( name: varchar("name").notNull(), // The title of the product title: varchar("title").notNull(), + // Potential image for the purchase item + imageUrl: varchar("image_url"), // The quantity of the product quantity: integer("quantity").notNull(), // Update infos @@ -101,5 +113,9 @@ export const purchaseItemTable = pgTable( }, (table) => ({ purchaseIdIdx: index("item_purchase_id_idx").on(table.purchaseId), + externalIdIdx: uniqueIndex("unique_external_purchase_item_id").on( + table.externalId, + table.purchaseId + ), }) ); diff --git a/packages/backend-elysia/src/domain/oracle/dto/WooCommerceWebhook.ts b/packages/backend-elysia/src/domain/oracle/dto/WooCommerceWebhook.ts new file mode 100644 index 000000000..49c491bfe --- /dev/null +++ b/packages/backend-elysia/src/domain/oracle/dto/WooCommerceWebhook.ts @@ -0,0 +1,35 @@ +export type WooCommerceOrderUpdateWebhookDto = Readonly<{ + id: number; // order_id + status: WooCommerceOrderStatus; // The financial status of the order (could include "paid", "refunded", etc.) + total: string; // Total price of the order + currency: string; // Currency code (ISO 4217) + date_created_gmt: string; // The creation date of the order + date_modified_gmt?: string; // The date when the order was last updated + date_completed_gmt?: string; // The date when the order was last updated + date_paid_gmt?: string; // The date when the order was last updated + customer_id: number; // The customer id + order_key: string; // The key of the order + transaction_id: string; // The id of the transaction + line_items: { + id: number; // The product id + product_id: number; // The product id + quantity: number; // The quantity of the product + price: number; // The price of the product + name: string; // The name of the product + image: { + id?: string; + src?: string; + }; + }[]; +}>; + +export type WooCommerceOrderStatus = + | "pending" + | "processing" + | "on-hold" + | "completed" + | "canncelled" + | "refunded" + | "failed" + | "trash" + | (string & {}); diff --git a/packages/backend-elysia/src/domain/oracle/index.ts b/packages/backend-elysia/src/domain/oracle/index.ts index 91c1d9a08..d1aad2d5d 100644 --- a/packages/backend-elysia/src/domain/oracle/index.ts +++ b/packages/backend-elysia/src/domain/oracle/index.ts @@ -3,11 +3,13 @@ import { Elysia } from "elysia"; import { updateMerkleRootJob } from "./jobs/updateOrale"; import { managmentRoutes } from "./routes/managment"; import { proofRoutes } from "./routes/proof"; -import { shopifyWebhook } from "./routes/shopifyWebhook"; +import { shopifyWebhook } from "./routes/webhook/shopifyWebhook"; +import { wooCommerceWebhook } from "./routes/webhook/wooCommerceWebhook"; export const oracle = new Elysia({ prefix: "/oracle" }) - .use(managmentRoutes) .use(shopifyWebhook) + .use(wooCommerceWebhook) + .use(managmentRoutes) .use(updateMerkleRootJob) .use(proofRoutes) .get( diff --git a/packages/backend-elysia/src/domain/oracle/routes/managment.ts b/packages/backend-elysia/src/domain/oracle/routes/managment.ts index ab98443eb..663c4eb55 100644 --- a/packages/backend-elysia/src/domain/oracle/routes/managment.ts +++ b/packages/backend-elysia/src/domain/oracle/routes/managment.ts @@ -45,6 +45,7 @@ export const managmentRoutes = new Elysia() // Return the oracle status return { setup: true, + platform: currentOracle.platform, webhookSigninKey: currentOracle.hookSignatureKey, stats: { firstPurchase: stats[0]?.firstPurchase ?? undefined, @@ -61,6 +62,11 @@ export const managmentRoutes = new Elysia() }), t.Object({ setup: t.Literal(true), + platform: t.Union([ + t.Literal("shopify"), + t.Literal("woocommerce"), + t.Literal("custom"), + ]), webhookSigninKey: t.String(), stats: t.Optional( t.Partial( @@ -84,7 +90,7 @@ export const managmentRoutes = new Elysia() return error(400, "Invalid product id"); } - const { hookSignatureKey } = body; + const { hookSignatureKey, platform } = body; // todo: Role check for the wallet @@ -94,11 +100,13 @@ export const managmentRoutes = new Elysia() .values({ productId, hookSignatureKey, + platform, }) .onConflictDoUpdate({ target: [productOracleTable.productId], set: { hookSignatureKey, + platform, }, }) .execute(); @@ -107,6 +115,11 @@ export const managmentRoutes = new Elysia() isAuthenticated: "business", body: t.Object({ hookSignatureKey: t.String(), + platform: t.Union([ + t.Literal("shopify"), + t.Literal("woocommerce"), + t.Literal("custom"), + ]), }), } ) diff --git a/packages/backend-elysia/src/domain/oracle/routes/shopifyWebhook.ts b/packages/backend-elysia/src/domain/oracle/routes/webhook/shopifyWebhook.ts similarity index 66% rename from packages/backend-elysia/src/domain/oracle/routes/shopifyWebhook.ts rename to packages/backend-elysia/src/domain/oracle/routes/webhook/shopifyWebhook.ts index a86bfd9fd..bdfaa90ce 100644 --- a/packages/backend-elysia/src/domain/oracle/routes/shopifyWebhook.ts +++ b/packages/backend-elysia/src/domain/oracle/routes/webhook/shopifyWebhook.ts @@ -4,20 +4,15 @@ import { isRunningInProd } from "@frak-labs/app-essentials"; import { eq } from "drizzle-orm"; import { Elysia } from "elysia"; import { concatHex, keccak256, toHex } from "viem"; -import { oracleContext } from "../context"; -import { - productOracleTable, - purchaseItemTable, - type purchaseStatusEnum, - purchaseStatusTable, -} from "../db/schema"; +import { productOracleTable, type purchaseStatusEnum } from "../../db/schema"; import type { OrderFinancialStatus, ShopifyOrderUpdateWebhookDto, -} from "../dto/ShopifyWebhook"; +} from "../../dto/ShopifyWebhook"; +import { purchaseWebhookService } from "../../services/hookService"; export const shopifyWebhook = new Elysia({ prefix: "/shopify" }) - .use(oracleContext) + .use(purchaseWebhookService) // Error failsafe, to never fail on shopify webhook .onError(({ error, code, body, path, headers }) => { log.error( @@ -61,11 +56,19 @@ export const shopifyWebhook = new Elysia({ prefix: "/shopify" }) // here we should just validate the request and save it .post( ":productId/hook", - async ({ params: { productId }, body, headers, oracleDb, error }) => { - // todo: hmac validation in the `onParse` hook? https://shopify.dev/docs/apps/build/webhooks/subscribe/https#step-2-validate-the-origin-of-your-webhook-to-ensure-its-coming-from-shopify - + async ({ + params: { productId }, + body, + headers, + error, + oracleDb, + validateBodyHmac, + upsertPurchase, + }) => { // Try to parse the body as a shopify webhook type and ensure the type validity - const webhookData = body as ShopifyOrderUpdateWebhookDto; + const webhookData = JSON.parse( + body + ) as ShopifyOrderUpdateWebhookDto; // Ensure the order id match the one in the headers if ( webhookData?.id !== @@ -89,6 +92,13 @@ export const shopifyWebhook = new Elysia({ prefix: "/shopify" }) return error(404, "Product oracle not found"); } + // Validate the body hmac + validateBodyHmac({ + body, + secret: oracle.hookSignatureKey, + signature: headers["x-shopify-hmac-sha256"], + }); + // Prebuild some data before insert const purchaseStatus = mapFinancialStatus( webhookData.financial_status @@ -109,52 +119,34 @@ export const shopifyWebhook = new Elysia({ prefix: "/shopify" }) ); // Insert purchase and items - await oracleDb.transaction(async (trx) => { - // Insert the purchase first - await trx - .insert(purchaseStatusTable) - .values({ - oracleId: oracle.id, - purchaseId, - externalId: webhookData.id.toString(), - externalCustomerId: webhookData.customer.id.toString(), - purchaseToken: - webhookData.checkout_token ?? webhookData.token, - status: purchaseStatus, - totalPrice: webhookData.total_price, - currencyCode: webhookData.currency, - }) - .onConflictDoUpdate({ - target: [purchaseStatusTable.purchaseId], - set: { - status: purchaseStatus, - totalPrice: webhookData.total_price, - currencyCode: webhookData.currency, - updatedAt: new Date(), - ...(webhookData.checkout_token - ? { - purchaseToken: webhookData.checkout_token, - } - : {}), - }, - }); - // Insert the items if needed - if (webhookData.line_items.length === 0) { - return; - } - const mappedItems = webhookData.line_items.map((item) => ({ + await upsertPurchase({ + purchase: { + oracleId: oracle.id, + purchaseId, + externalId: webhookData.id.toString(), + externalCustomerId: webhookData.customer.id.toString(), + purchaseToken: + webhookData.checkout_token ?? webhookData.token, + status: purchaseStatus, + totalPrice: webhookData.total_price, + currencyCode: webhookData.currency, + }, + purchaseItems: webhookData.line_items.map((item) => ({ purchaseId, externalId: item.product_id.toString(), price: item.price, name: item.name, title: item.title, quantity: item.quantity, - })); - await trx.insert(purchaseItemTable).values(mappedItems); + })), }); // Return the success state return "ok"; + }, + { + type: "text", + body: t.String(), } ); diff --git a/packages/backend-elysia/src/domain/oracle/routes/webhook/wooCommerceWebhook.ts b/packages/backend-elysia/src/domain/oracle/routes/webhook/wooCommerceWebhook.ts new file mode 100644 index 000000000..3f079fd63 --- /dev/null +++ b/packages/backend-elysia/src/domain/oracle/routes/webhook/wooCommerceWebhook.ts @@ -0,0 +1,143 @@ +import { log } from "@backend-common"; +import { t } from "@backend-utils"; +import { eq } from "drizzle-orm"; +import { Elysia } from "elysia"; +import { concatHex, keccak256, toHex } from "viem"; +import { productOracleTable, type purchaseStatusEnum } from "../../db/schema"; +import type { + WooCommerceOrderStatus, + WooCommerceOrderUpdateWebhookDto, +} from "../../dto/WooCommerceWebhook"; +import { purchaseWebhookService } from "../../services/hookService"; + +export const wooCommerceWebhook = new Elysia({ prefix: "/woocommerce" }) + .use(purchaseWebhookService) + // Error failsafe, to never fail on shopify webhook + .onError(({ error, code, body, path, headers, response }) => { + log.error( + { + error, + code, + reqPath: path, + reqBody: body, + reqHeaders: headers, + response, + }, + "Error while handling WooCommerce webhook" + ); + return new Response("ko", { status: 200 }); + }) + .guard({ + headers: t.Partial( + t.Object({ + "x-wc-webhook-source": t.String(), + "x-wc-webhook-topic": t.String(), + "x-wc-webhook-resource": t.String(), + "x-wc-webhook-event": t.String(), + "x-wc-webhook-signature": t.String(), + "x-wc-webhook-id": t.String(), + "x-wc-webhook-delivery-id": t.String(), + }) + ), + }) + // Request pre validation hook + .onBeforeHandle(({ headers, error }) => { + if (!headers["x-wc-webhook-signature"]) { + return error(400, "Missing signature"); + } + if (headers["x-wc-webhook-resource"] !== "order") { + return error(400, "Unsupported woo commerce webhook"); + } + }) + // Shopify only give us 5sec to answer, all the heavy logic should be in a cron running elsewhere, + // here we should just validate the request and save it + .post( + ":productId/hook", + async ({ + // Query + params: { productId }, + body, + headers, + error, + // Context + oracleDb, + upsertPurchase, + validateBodyHmac, + }) => { + // Try to parse the body as a shopify webhook type and ensure the type validity + const webhookData = JSON.parse( + body + ) as WooCommerceOrderUpdateWebhookDto; + + // Find the product oracle for this product id + if (!productId) { + return error(400, "Missing product id"); + } + const oracle = await oracleDb.query.productOracleTable.findFirst({ + where: eq(productOracleTable.productId, productId), + }); + if (!oracle) { + return error(404, "Product oracle not found"); + } + + // Validate the body hmac + validateBodyHmac({ + body, + secret: oracle.hookSignatureKey, + signature: headers["x-wc-webhook-signature"], + }); + + // Prebuild some data before insert + const purchaseStatus = mapOrderStatus(webhookData.status); + const purchaseId = keccak256( + concatHex([oracle.productId, toHex(webhookData.id)]) + ); + + // Insert purchase and items + await upsertPurchase({ + purchase: { + oracleId: oracle.id, + purchaseId, + externalId: webhookData.id.toString(), + externalCustomerId: webhookData.customer_id.toString(), + purchaseToken: + webhookData.order_key ?? webhookData.transaction_id, + status: purchaseStatus, + totalPrice: webhookData.total, + currencyCode: webhookData.currency, + }, + purchaseItems: webhookData.line_items.map((item) => ({ + purchaseId, + externalId: item.product_id.toString(), + price: item.price.toString(), + name: item.name, + title: item.name, + quantity: item.quantity, + imageUrl: item.image?.src?.length ? item.image.src : null, + })), + }); + + // Return the success state + return "ok"; + }, + { + type: "text", + body: t.String(), + } + ); + +function mapOrderStatus( + orderStatus: WooCommerceOrderStatus +): (typeof purchaseStatusEnum.enumValues)[number] { + if (orderStatus === "completed") { + return "confirmed"; + } + if (orderStatus === "refunded") { + return "refunded"; + } + if (orderStatus === "cancelled") { + return "cancelled"; + } + + return "pending"; +} diff --git a/packages/backend-elysia/src/domain/oracle/services/hookService.ts b/packages/backend-elysia/src/domain/oracle/services/hookService.ts new file mode 100644 index 000000000..5520a2971 --- /dev/null +++ b/packages/backend-elysia/src/domain/oracle/services/hookService.ts @@ -0,0 +1,113 @@ +import { log } from "@backend-common"; +import { CryptoHasher } from "bun"; +import Elysia from "elysia"; +import { oracleContext } from "../context"; +import { purchaseItemTable, purchaseStatusTable } from "../db/schema"; + +export const purchaseWebhookService = new Elysia({ + name: "Service.PurchaseWebhook", +}) + .use(oracleContext) + .decorate(({ oracleDb, ...decorators }) => { + /** + * Validate a body hmac signature + */ + function validateBodyHmac({ + body, + secret, + signature, + }: { + body: string; + secret: string; + signature?: string; + }) { + // hmac hash of the body + const hasher = new CryptoHasher("sha256", secret); + hasher.update(body); + + // Convert both to buffer + const recomputedSignature = hasher.digest(); + const baseSignature = Buffer.from(signature ?? "", "base64"); + + // Compare the two + if (!baseSignature.equals(recomputedSignature)) { + log.warn( + { + signature, + baseSignature: baseSignature.toString("hex"), + recomputedSignatureHex: + recomputedSignature.toString("hex"), + recomputedSignatureB64: + recomputedSignature.toString("base64"), + }, + "Signature mismatch" + ); + } else { + log.debug( + { + recomputedSignature: + recomputedSignature.toString("hex"), + baseSignature: baseSignature.toString("hex"), + }, + "Signature matches" + ); + } + } + + /** + * Upsert a purchase in the database + * @returns + */ + async function upsertPurchase({ + purchase, + purchaseItems, + }: { + purchase: typeof purchaseStatusTable.$inferInsert; + purchaseItems: (typeof purchaseItemTable.$inferInsert)[]; + }) { + const dbId = await oracleDb.transaction(async (trx) => { + // Insert the purchase first + const insertedId = await trx + .insert(purchaseStatusTable) + .values(purchase) + .onConflictDoUpdate({ + target: [purchaseStatusTable.purchaseId], + set: { + status: purchase.status, + totalPrice: purchase.totalPrice, + currencyCode: purchase.currencyCode, + // Reset leaf on update + leaf: null, + updatedAt: new Date(), + ...(purchase.purchaseToken + ? { + purchaseToken: purchase.purchaseToken, + } + : {}), + }, + }) + .returning({ purchaseId: purchaseStatusTable.id }); + + // Insert the items if needed + if (purchaseItems.length > 0) { + await trx + .insert(purchaseItemTable) + .values(purchaseItems) + .onConflictDoNothing(); + } + + return insertedId; + }); + log.debug( + { purchase, purchaseItems, insertedId: dbId }, + "Purchase upserted" + ); + } + + return { + ...decorators, + oracleDb, + upsertPurchase, + validateBodyHmac, + }; + }); diff --git a/packages/dashboard/src/module/common/utils/useWaitForTxAndInvalidateQueries.ts b/packages/dashboard/src/module/common/utils/useWaitForTxAndInvalidateQueries.ts index 3b266ebe2..ceae136dc 100644 --- a/packages/dashboard/src/module/common/utils/useWaitForTxAndInvalidateQueries.ts +++ b/packages/dashboard/src/module/common/utils/useWaitForTxAndInvalidateQueries.ts @@ -12,13 +12,17 @@ export function useWaitForTxAndInvalidateQueries() { const queryClient = useQueryClient(); return useCallback( - async ({ hash, queryKey }: { hash: Hex; queryKey: string[] }) => { + async ({ + hash, + queryKey, + confirmations = 16, + }: { hash: Hex; queryKey: string[]; confirmations?: number }) => { // Wait a bit for the tx to be confirmed await guard(() => waitForTransactionReceipt(viemClient, { hash, - confirmations: 32, - retryCount: 32, + confirmations: confirmations, + retryCount: confirmations, }) ); diff --git a/packages/dashboard/src/module/dashboard/component/ButtonAddProduct/index.tsx b/packages/dashboard/src/module/dashboard/component/ButtonAddProduct/index.tsx index eed8dcca2..a845cfcd8 100644 --- a/packages/dashboard/src/module/dashboard/component/ButtonAddProduct/index.tsx +++ b/packages/dashboard/src/module/dashboard/component/ButtonAddProduct/index.tsx @@ -1,6 +1,5 @@ import { AlertDialog } from "@/module/common/component/AlertDialog"; import { Row } from "@/module/common/component/Row"; -import { useWaitForTxAndInvalidateQueries } from "@/module/common/utils/useWaitForTxAndInvalidateQueries"; import { ProductItem } from "@/module/dashboard/component/ProductItem"; import { useCheckDomainName, @@ -27,7 +26,6 @@ import { Spinner } from "@module/component/Spinner"; import { TextWithCopy } from "@module/component/TextWithCopy"; import { Input } from "@module/component/forms/Input"; import { validateUrl } from "@module/utils/validateUrl"; -import { useQuery } from "@tanstack/react-query"; import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { BadgeCheck, Plus } from "lucide-react"; import { useEffect, useState } from "react"; @@ -373,32 +371,17 @@ function NewProductVerify({ setupCode: string; }) { const setIsMinting = useSetAtom(isMintingAtom); - const waitForTxAndInvalidateQueries = useWaitForTxAndInvalidateQueries(); const { - mutate: triggerMintMyContent, - isIdle, - error, - data: { mintTxHash } = {}, + infoTxt, + mutation: { + mutate: triggerMintMyContent, + isIdle, + error, + data: { mintTxHash } = {}, + }, } = useMintMyProduct(); - const { isLoading: isWaitingForFinalisedCreation, data: isConfirmed } = - useQuery({ - queryKey: ["mint", "wait-for-finalised-deployment"], - enabled: !!mintTxHash, - queryFn: async () => { - if (!mintTxHash) return false; - - // Invalidate the product related cache - await waitForTxAndInvalidateQueries({ - hash: mintTxHash, - queryKey: ["product"], - }); - setIsMinting(false); - return true; - }, - }); - if (!domain) return null; return ( @@ -417,12 +400,19 @@ function NewProductVerify({ className={styles.newProductForm__fingerprint} action={() => { setIsMinting(true); - triggerMintMyContent({ - name, - domain, - productTypes, - setupCode, - }); + triggerMintMyContent( + { + name, + domain, + productTypes, + setupCode, + }, + { + onSettled() { + setIsMinting(false); + }, + } + ); }} disabled={!isIdle} > @@ -430,11 +420,7 @@ function NewProductVerify({ {error &&
{error.message}
} -- Setting all the right blockchain data + {infoTxt} ...
); } + if (!txHash) return null; + return ( <>
diff --git a/packages/dashboard/src/module/dashboard/hooks/useMintMyProduct.ts b/packages/dashboard/src/module/dashboard/hooks/useMintMyProduct.ts
index f5c773a44..ae27741fb 100644
--- a/packages/dashboard/src/module/dashboard/hooks/useMintMyProduct.ts
+++ b/packages/dashboard/src/module/dashboard/hooks/useMintMyProduct.ts
@@ -1,18 +1,42 @@
-import { useSetupInteractionContract } from "@/module/product/hook/useSetupInteractionContract";
-import type { ProductTypesKey } from "@frak-labs/nexus-sdk/core";
+import { useGetAdminWallet } from "@/module/common/hook/useGetAdminWallet";
+import { useWaitForTxAndInvalidateQueries } from "@/module/common/utils/useWaitForTxAndInvalidateQueries";
+import {
+ addresses,
+ campaignBankAbi,
+ interactionValidatorRoles,
+ productAdministratorRegistryAbi,
+ productInteractionManagerAbi,
+ productRoles,
+} from "@frak-labs/app-essentials/blockchain";
+import type {
+ ProductTypesKey,
+ SendTransactionModalStepType,
+} from "@frak-labs/nexus-sdk/core";
+import { useSendTransactionAction } from "@frak-labs/nexus-sdk/react";
import { backendApi } from "@frak-labs/shared/context/server";
import { useMutation } from "@tanstack/react-query";
+import { useState } from "react";
+import { encodeFunctionData } from "viem";
/**
* Hook to mint the user product
*/
export function useMintMyProduct() {
- const { mutateAsync: deployInteractionContract } =
- useSetupInteractionContract();
+ const { data: oracleUpdater } = useGetAdminWallet({
+ key: "oracle-updater",
+ });
+ const { mutateAsync: sendTransaction } = useSendTransactionAction();
+ const waitForTxAndInvalidateQueries = useWaitForTxAndInvalidateQueries();
+
+ const [infoTxt, setInfoTxt] = useState
+
The purchase tracker will permit to create campaigns and
distribute rewards based on user purchase on your website.
-
- Webhook URL to use in your shopify notification centers:{" "}
-
+ To register the webhook on WooCommerce, go to your WordPress
+ admin console, and then in
+ And finally Register it on Frak via this button
+ To register the webhook on Shopify, go to your Shopify admin
+ console, and then in {oracleSetupData.webhookUrl}
-
+
+ WooCommerce {">"} Settings {">"} Advanced {">"} Webhooks
+
+
+ Create a new WebHook with the topic Order Updated with
+ the following URL and Secret:
+ {webhookUrl}
+ {signinKey}
+
+
+ Settings {">"} Notifications {">"} WebHook
+
+
+ Create a new WebHook with the event Order Updated with
+ the following URL:
+ {webhookUrl}
+