diff --git a/.gitignore b/.gitignore index 49828b91e..0f9a65343 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,5 @@ cypress/videos # Local address book address-book.json +modules/server-node/connext-store.db +modules/server-node/migrations/migrate.lock diff --git a/modules/server-node/migrations/20200911091549-init/README.md b/modules/server-node/migrations/20200911091549-init/README.md new file mode 100644 index 000000000..1a366f3bd --- /dev/null +++ b/modules/server-node/migrations/20200911091549-init/README.md @@ -0,0 +1,122 @@ +# Migration `20200911091549-init` + +This migration has been generated by Rahul Sethuram at 9/11/2020, 11:15:49 AM. +You can check out the [state of the schema](./schema.prisma) after the migration. + +## Database Steps + +```sql +CREATE TABLE "balance" ( + "participant" TEXT NOT NULL, + "assetId" TEXT NOT NULL, + "to" TEXT NOT NULL, + "amount" TEXT NOT NULL, + "lockedValue" TEXT NOT NULL, + "channelAddress" TEXT NOT NULL, + + FOREIGN KEY ("channelAddress") REFERENCES "channel"("channelAddress") ON DELETE CASCADE ON UPDATE CASCADE, +PRIMARY KEY ("participant","channelAddress","assetId") +) + +CREATE TABLE "channel" ( + "channelAddress" TEXT NOT NULL, + "publicIdentifierA" TEXT NOT NULL, + "publicIdentifierB" TEXT NOT NULL, + "participantA" TEXT NOT NULL, + "participantB" TEXT NOT NULL, + "timeout" TEXT NOT NULL, + "nonce" INTEGER NOT NULL, + "latestDepositNonce" INTEGER NOT NULL, + "merkleRoot" TEXT NOT NULL, + "channelFactoryAddress" TEXT NOT NULL, + "vectorChannelMastercopyAddress" TEXT NOT NULL, + "chainId" INTEGER NOT NULL, + "providerUrl" TEXT NOT NULL, +PRIMARY KEY ("channelAddress") +) + +CREATE TABLE "update" ( + "transferId" TEXT NOT NULL, + "assetId" TEXT NOT NULL, + "transferDefinition" TEXT NOT NULL, + "transferTimeout" TEXT NOT NULL, + "transferEncodings" TEXT NOT NULL, + "merkleProofData" TEXT NOT NULL, + "signatureA" TEXT, + "signatureB" TEXT, + "adjudicatorAddress" TEXT NOT NULL, + "channelAddress" TEXT NOT NULL, + + FOREIGN KEY ("channelAddress") REFERENCES "channel"("channelAddress") ON DELETE CASCADE ON UPDATE CASCADE, +PRIMARY KEY ("transferId") +) + +CREATE UNIQUE INDEX "update_channelAddress_unique" ON "update"("channelAddress") +``` + +## Changes + +```diff +diff --git schema.prisma schema.prisma +migration ..20200911091549-init +--- datamodel.dml ++++ datamodel.dml +@@ -1,0 +1,55 @@ ++generator client { ++ provider = "prisma-client-js" ++ binaryTargets = ["native"] ++} ++ ++datasource db { ++ provider = ["sqlite", "postgresql"] ++ url = "***" ++} ++ ++model Balance { ++ @@map(name: "balance") ++ participant String ++ assetId String ++ to String ++ amount String ++ lockedValue String ++ Channel Channel @relation(fields: [channelAddress], references: [channelAddress]) ++ channelAddress String ++ @@id([participant, channelAddress, assetId]) ++} ++ ++model Channel { ++ @@map(name: "channel") ++ channelAddress String @id ++ publicIdentifierA String ++ publicIdentifierB String ++ participantA String ++ participantB String ++ timeout String ++ nonce Int ++ latestDepositNonce Int ++ merkleRoot String ++ balances Balance[] ++ channelFactoryAddress String ++ vectorChannelMastercopyAddress String ++ chainId Int ++ providerUrl String ++ latestUpdate Update? ++} ++ ++model Update { ++ @@map(name: "update") ++ transferId String @id ++ assetId String ++ transferDefinition String ++ transferTimeout String ++ transferEncodings String ++ merkleProofData String ++ signatureA String? ++ signatureB String? ++ adjudicatorAddress String ++ channel Channel @relation(fields: [channelAddress], references: [channelAddress]) ++ channelAddress String ++} +``` + + diff --git a/modules/server-node/migrations/20200911091549-init/schema.prisma b/modules/server-node/migrations/20200911091549-init/schema.prisma new file mode 100644 index 000000000..063ecc813 --- /dev/null +++ b/modules/server-node/migrations/20200911091549-init/schema.prisma @@ -0,0 +1,55 @@ +generator client { + provider = "prisma-client-js" + binaryTargets = ["native"] +} + +datasource db { + provider = ["sqlite", "postgresql"] + url = "***" +} + +model Balance { + @@map(name: "balance") + participant String + assetId String + to String + amount String + lockedValue String + Channel Channel @relation(fields: [channelAddress], references: [channelAddress]) + channelAddress String + @@id([participant, channelAddress, assetId]) +} + +model Channel { + @@map(name: "channel") + channelAddress String @id + publicIdentifierA String + publicIdentifierB String + participantA String + participantB String + timeout String + nonce Int + latestDepositNonce Int + merkleRoot String + balances Balance[] + channelFactoryAddress String + vectorChannelMastercopyAddress String + chainId Int + providerUrl String + latestUpdate Update? +} + +model Update { + @@map(name: "update") + transferId String @id + assetId String + transferDefinition String + transferTimeout String + transferEncodings String + merkleProofData String + signatureA String? + signatureB String? + adjudicatorAddress String + channel Channel @relation(fields: [channelAddress], references: [channelAddress]) + channelAddress String +} diff --git a/modules/server-node/migrations/20200911091549-init/steps.json b/modules/server-node/migrations/20200911091549-init/steps.json new file mode 100644 index 000000000..3ef6360b8 --- /dev/null +++ b/modules/server-node/migrations/20200911091549-init/steps.json @@ -0,0 +1,462 @@ +{ + "version": "0.3.14-fixed", + "steps": [ + { + "tag": "CreateSource", + "source": "db" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Source", + "source": "db" + }, + "argument": "provider", + "value": "[\"sqlite\", \"postgresql\"]" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Source", + "source": "db" + }, + "argument": "url", + "value": "\"***\"" + }, + { + "tag": "CreateModel", + "model": "Balance" + }, + { + "tag": "CreateField", + "model": "Balance", + "field": "participant", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Balance", + "field": "assetId", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Balance", + "field": "to", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Balance", + "field": "amount", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Balance", + "field": "lockedValue", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Balance", + "field": "Channel", + "type": "Channel", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Balance", + "field": "Channel" + }, + "directive": "relation" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Balance", + "field": "Channel" + }, + "directive": "relation" + }, + "argument": "fields", + "value": "[channelAddress]" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Balance", + "field": "Channel" + }, + "directive": "relation" + }, + "argument": "references", + "value": "[channelAddress]" + }, + { + "tag": "CreateField", + "model": "Balance", + "field": "channelAddress", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Model", + "model": "Balance" + }, + "directive": "map" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Model", + "model": "Balance" + }, + "directive": "map" + }, + "argument": "name", + "value": "\"balance\"" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Model", + "model": "Balance" + }, + "directive": "id" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Model", + "model": "Balance" + }, + "directive": "id" + }, + "argument": "", + "value": "[participant, channelAddress, assetId]" + }, + { + "tag": "CreateModel", + "model": "Channel" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "channelAddress", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Channel", + "field": "channelAddress" + }, + "directive": "id" + } + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "publicIdentifierA", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "publicIdentifierB", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "participantA", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "participantB", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "timeout", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "nonce", + "type": "Int", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "latestDepositNonce", + "type": "Int", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "merkleRoot", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "balances", + "type": "Balance", + "arity": "List" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "channelFactoryAddress", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "vectorChannelMastercopyAddress", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "chainId", + "type": "Int", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "providerUrl", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Channel", + "field": "latestUpdate", + "type": "Update", + "arity": "Optional" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Model", + "model": "Channel" + }, + "directive": "map" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Model", + "model": "Channel" + }, + "directive": "map" + }, + "argument": "name", + "value": "\"channel\"" + }, + { + "tag": "CreateModel", + "model": "Update" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "transferId", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Update", + "field": "transferId" + }, + "directive": "id" + } + }, + { + "tag": "CreateField", + "model": "Update", + "field": "assetId", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "transferDefinition", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "transferTimeout", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "transferEncodings", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "merkleProofData", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "signatureA", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "signatureB", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "adjudicatorAddress", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "channel", + "type": "Channel", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Update", + "field": "channel" + }, + "directive": "relation" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Update", + "field": "channel" + }, + "directive": "relation" + }, + "argument": "fields", + "value": "[channelAddress]" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Update", + "field": "channel" + }, + "directive": "relation" + }, + "argument": "references", + "value": "[channelAddress]" + }, + { + "tag": "CreateField", + "model": "Update", + "field": "channelAddress", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Model", + "model": "Update" + }, + "directive": "map" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Model", + "model": "Update" + }, + "directive": "map" + }, + "argument": "name", + "value": "\"update\"" + } + ] +} \ No newline at end of file diff --git a/modules/server-node/package.json b/modules/server-node/package.json index 2e3df25e7..ae5e2cc2a 100644 --- a/modules/server-node/package.json +++ b/modules/server-node/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "prisma generate && rm -rf dist && tsc -p tsconfig.json", "start": "prisma migrate up --experimental && node dist/index.js", - "test": "prisma generate && prisma migrate up --experimental && ts-mocha --bail --check-leaks --exit --timeout 60000 'src/**/*.spec.ts'" + "test": "prisma generate && prisma migrate up --experimental && ts-mocha --bail --check-leaks --exit --timeout 60000 'src/**/*.spec.ts'", + "migration:generate": "prisma migrate save --experimental" }, "author": "", "license": "ISC", diff --git a/modules/server-node/schema.prisma b/modules/server-node/schema.prisma index 0da274dec..6b59daf44 100644 --- a/modules/server-node/schema.prisma +++ b/modules/server-node/schema.prisma @@ -10,14 +10,14 @@ datasource db { model Balance { @@map(name: "balance") - id String @id @default(uuid()) participant String assetId String to String amount String lockedValue String - Channel Channel? @relation(fields: [channelAddress], references: [channelAddress]) - channelAddress String? + Channel Channel @relation(fields: [channelAddress], references: [channelAddress]) + channelAddress String + @@id([participant, channelAddress, assetId]) } model Channel { @@ -45,7 +45,6 @@ model Update { assetId String transferDefinition String transferTimeout String - transferStateHash Int transferEncodings String merkleProofData String signatureA String? diff --git a/modules/server-node/src/services/store.spec.ts b/modules/server-node/src/services/store.spec.ts index 5f9b6ef9a..be848730b 100644 --- a/modules/server-node/src/services/store.spec.ts +++ b/modules/server-node/src/services/store.spec.ts @@ -1,15 +1,25 @@ -import { IStoreService } from "@connext/vector-types"; - import { createTestChannelState } from "../test/utils/channel"; import { expect } from "../test/utils/assert"; -import { Store } from "./store"; +import { PrismaStore } from "./store"; describe("store", () => { - let store: IStoreService; + let store: PrismaStore; + before(() => { - store = new Store(); + store = new PrismaStore(); + }); + + afterEach(async () => { + await store.prisma.balance.deleteMany({}); + await store.prisma.update.deleteMany({}); + await store.prisma.channel.deleteMany({}); }); + + after(async () => { + await store.disconnect(); + }); + it("should save a channel update", async () => { const state = createTestChannelState(); await store.saveChannelState(state); diff --git a/modules/server-node/src/services/store.ts b/modules/server-node/src/services/store.ts index 46c615ffa..e075e2cda 100644 --- a/modules/server-node/src/services/store.ts +++ b/modules/server-node/src/services/store.ts @@ -1,17 +1,147 @@ -import { IStoreService, FullChannelState, CoreTransferState } from "@connext/vector-types"; -import { PrismaClient } from "@prisma/client"; +import { IStoreService, FullChannelState, CoreTransferState, CreateUpdateDetails } from "@connext/vector-types"; +import { + BalanceCreateWithoutChannelInput, + BalanceUpsertWithWhereUniqueWithoutChannelInput, + PrismaClient, +} from "@prisma/client"; -export class Store implements IStoreService { - private prisma: PrismaClient; +export class PrismaStore implements IStoreService { + public prisma: PrismaClient; constructor() { this.prisma = new PrismaClient(); } - getChannelState(channelAddress: string): Promise | undefined> { - throw new Error("Method not implemented."); + connect(): Promise { + return Promise.resolve(); } - saveChannelState(channelState: FullChannelState): Promise { - throw new Error("Method not implemented."); + async disconnect(): Promise { + await this.prisma.$disconnect(); + } + + async getChannelState(channelAddress: string): Promise | undefined> { + const channelEntity = await this.prisma.channel.findOne({ + where: { channelAddress }, + include: { balances: true, latestUpdate: true }, + }); + console.log("channelEntity: ", channelEntity); + return channelEntity as any; + } + + async saveChannelState(channelState: FullChannelState): Promise { + const channel = await this.prisma.channel.upsert({ + where: { channelAddress: channelState.channelAddress }, + create: { + chainId: channelState.networkContext.chainId, + channelAddress: channelState.channelAddress, + channelFactoryAddress: channelState.networkContext.channelFactoryAddress, + latestDepositNonce: channelState.latestDepositNonce, + merkleRoot: channelState.merkleRoot, + nonce: channelState.nonce, + participantA: channelState.participants[0], + participantB: channelState.participants[1], + providerUrl: channelState.networkContext.providerUrl, + publicIdentifierA: channelState.publicIdentifiers[0], + publicIdentifierB: channelState.publicIdentifiers[1], + timeout: channelState.timeout, + vectorChannelMastercopyAddress: channelState.networkContext.vectorChannelMastercopyAddress, + balances: { + create: channelState.assetIds.reduce((cre: BalanceCreateWithoutChannelInput[], assetId, index) => { + return [ + ...cre, + { + amount: channelState.balances[index].amount[0], + lockedValue: channelState.lockedValue[index].amount, + participant: channelState.participants[0], + to: channelState.balances[index].to[0], + assetId, + }, + { + amount: channelState.balances[index].amount[1], + lockedValue: channelState.lockedValue[index].amount, + participant: channelState.participants[1], + to: channelState.balances[index].to[1], + assetId, + }, + ]; + }, []), + }, + latestUpdate: channelState.latestUpdate && { + create: { + adjudicatorAddress: channelState.networkContext.adjudicatorAddress, + assetId: channelState.latestUpdate!.assetId, + merkleProofData: channelState.latestUpdate!.details.merkleProofData, + transferDefinition: channelState.latestUpdate!.details.transferDefinition, + transferEncodings: JSON.stringify( + (channelState.latestUpdate!.details as CreateUpdateDetails).transferEncodings, + ), + transferId: channelState.latestUpdate!.details.transferId, + transferStateHash: channelState.latestUpdate!.details.transferStateHash, + transferTimeout: channelState.latestUpdate!.details.transferTimeout, + signatureA: channelState.latestUpdate!.signatures[0], + signatureB: channelState.latestUpdate!.signatures[1], + }, + }, + }, + update: { + latestDepositNonce: channelState.latestDepositNonce, + merkleRoot: channelState.merkleRoot, + nonce: channelState.nonce, + balances: { + upsert: channelState.assetIds.reduce( + (upsert: BalanceUpsertWithWhereUniqueWithoutChannelInput[], assetId, index) => { + return [ + ...upsert, + { + create: { + amount: channelState.balances[index].amount[0], + lockedValue: channelState.lockedValue[index].amount, + participant: channelState.participants[0], + to: channelState.balances[index].to[0], + assetId, + }, + update: { + amount: channelState.balances[index].amount[0], + lockedValue: channelState.lockedValue[index].amount, + to: channelState.balances[index].to[0], + }, + where: { + participant_channelAddress_assetId: { + participant: channelState.participants[0], + channelAddress: channelState.channelAddress, + assetId, + }, + }, + }, + { + create: { + amount: channelState.balances[index].amount[1], + lockedValue: channelState.lockedValue[index].amount, + participant: channelState.participants[1], + to: channelState.balances[index].to[1], + assetId, + }, + update: { + amount: channelState.balances[index].amount[1], + lockedValue: channelState.lockedValue[index].amount, + to: channelState.balances[index].to[1], + }, + where: { + participant_channelAddress_assetId: { + participant: channelState.participants[1], + channelAddress: channelState.channelAddress, + assetId, + }, + }, + }, + ]; + }, + [], + ), + }, + }, + include: { balances: true, latestUpdate: true }, + }); + console.log("channel: ", channel); } getTransferInitialStates(channelAddress: string): Promise { throw new Error("Method not implemented."); diff --git a/modules/server-node/src/test/utils/channel.ts b/modules/server-node/src/test/utils/channel.ts index df06870c0..376fbeb8e 100644 --- a/modules/server-node/src/test/utils/channel.ts +++ b/modules/server-node/src/test/utils/channel.ts @@ -29,20 +29,17 @@ export const createTestChannelState = (): FullChannelState => { return { assetIds: ["0x0000000000000000000000000000000000000000", "0x1000000000000000000000000000000000000000"], balances: [ - // participantA + // assetId0 { amount: ["1", "2"], to: ["0xa000000000000000000000000000000000000000", "0xb000000000000000000000000000000000000000"], }, - // participantB + // assetId1 { amount: ["1", "2"], to: ["0xa000000000000000000000000000000000000000", "0xb000000000000000000000000000000000000000"], }, ], - channelAddress: "0x1110000000000000000000000000000000000000", - latestDepositNonce: 1, - latestUpdate: createTestChannelUpdate(), lockedValue: [ { amount: "1", @@ -51,6 +48,9 @@ export const createTestChannelState = (): FullChannelState => { amount: "2", }, ], + channelAddress: "0x1110000000000000000000000000000000000000", + latestDepositNonce: 1, + latestUpdate: createTestChannelUpdate(), merkleRoot: "0x", networkContext: { chainId: 1337, diff --git a/modules/types/src/store.ts b/modules/types/src/store.ts index f55b0e1aa..1c4dc37b4 100644 --- a/modules/types/src/store.ts +++ b/modules/types/src/store.ts @@ -1,6 +1,8 @@ import { FullChannelState, CoreTransferState, TransferState } from "./channel"; export interface IStoreService { + connect(): Promise; + disconnect(): Promise; getChannelState(channelAddress: string): Promise; saveChannelState(channelState: FullChannelState): Promise;