diff --git a/apollo/prisma/migrations/20240417090922_init/migration.sql b/apollo/prisma/migrations/20240417090922_init/migration.sql new file mode 100644 index 00000000..6428d879 --- /dev/null +++ b/apollo/prisma/migrations/20240417090922_init/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "LnBridgeRelayInfo" ADD COLUMN "dynamicFee" TEXT, +ADD COLUMN "dynamicFeeExpire" TEXT, +ADD COLUMN "dynamicFeeSignature" TEXT; diff --git a/apollo/prisma/schema.prisma b/apollo/prisma/schema.prisma index 14ee97c2..aeaaaaa2 100644 --- a/apollo/prisma/schema.prisma +++ b/apollo/prisma/schema.prisma @@ -84,5 +84,8 @@ model LnBridgeRelayInfo { transferLimit String softTransferLimit String paused Boolean + dynamicFee String? + dynamicFeeExpire String? + dynamicFeeSignature String? } diff --git a/apollo/src/aggregation/aggregation.history.graphql b/apollo/src/aggregation/aggregation.history.graphql index 3e8d7f76..12131050 100644 --- a/apollo/src/aggregation/aggregation.history.graphql +++ b/apollo/src/aggregation/aggregation.history.graphql @@ -77,6 +77,9 @@ type LnBridgeRelayInfo { transferLimit: String softTransferLimit: String paused: Boolean + dynamicFee: String + dynamicFeeExpire: String + dynamicFeeSignature: String } type LnBridgeRelayInfos { @@ -119,5 +122,8 @@ type Mutation { addGuardSignature(id: String, dataHash: String, signature: String): String updateConfirmedBlock(id: String, block: String): String lnBridgeHeartBeat(fromChainId: String, toChainId: String, version: String, relayer: String, tokenAddress: String, softTransferLimit: String): String + signConfirmedBlock(id: String, relayer: String, block: String, timestamp: Int, signature: String): String + signHeartBeat(fromChainId: String, toChainId: String, version: String, relayer: String, tokenAddress: String, softTransferLimit: String, timestamp: Int, signature: String): String + signDynamicFee(fromChainId: String, toChainId: String, version: String, relayer: String, tokenAddress: String, dynamicFee: String, dynamicFeeExpire: String, dynamicFeeSignature: String, timestamp: Int, signature: String): String } diff --git a/apollo/src/aggregation/aggregation.resolver.ts b/apollo/src/aggregation/aggregation.resolver.ts index 873bb215..13c32517 100644 --- a/apollo/src/aggregation/aggregation.resolver.ts +++ b/apollo/src/aggregation/aggregation.resolver.ts @@ -2,12 +2,49 @@ import { Args, Query, Mutation, Resolver } from '@nestjs/graphql'; import { isEmpty, isNull, isUndefined } from 'lodash'; import { AggregationService } from './aggregation.service'; import { Prisma } from '@prisma/client'; +import * as ethUtil from 'ethereumjs-util'; +import Web3 from 'web3'; @Resolver() export class AggregationResolver { + private readonly web3 = new Web3(Web3.givenProvider); private readonly heartbeatTimeout = 300; + private readonly signatureExpire = 120; + // todo: move this to contract + private readonly relayerProxy = { + }; constructor(private aggregationService: AggregationService) {} + private ecrecover(hash: string, sig: string): string { + const sigObj = ethUtil.fromRpcSig(sig); + const pubkey = ethUtil.ecrecover( + Buffer.from(hash.substr(2), 'hex'), + sigObj.v, + sigObj.r, + sigObj.s + ); + return ethUtil.bufferToHex(ethUtil.publicToAddress(pubkey)).toLowerCase(); + } + + private checkMessageSender(timestamp: number, message: string, relayer: string, sig: string): boolean { + try { + const now = Math.floor(Date.now() / 1000); + if (timestamp + this.signatureExpire < now) { + return false; + } + + const messageHash = this.web3.utils.soliditySha3({value: `${timestamp}`, type: 'uint256'}, {value: message, type: 'string'}); + const dataHash = this.web3.utils.soliditySha3( + { value: '\x19Ethereum Signed Message:\n32', type: 'string' }, + { value: messageHash, type: 'bytes' } + ); + const signer = this.ecrecover(dataHash, sig); + return signer === relayer || this.relayerProxy[signer] === relayer; + } catch { + return false; + } + } + @Query() async historyRecordById(@Args('id') id: string) { return this.aggregationService.queryHistoryRecordById({ @@ -161,14 +198,23 @@ export class AggregationResolver { }); } + /** + * @deprecated instead, please use signConfirmedBlock + **/ @Mutation() - async updateConfirmedBlock(@Args('id') id: string, @Args('block') block: string) { + async updateConfirmedBlock( + @Args('id') id: string, + @Args('block') block: string + ) { await this.aggregationService.updateConfirmedBlock({ where: { id: id }, block: block, }); } + /** + * @deprecated instead, please use signHeartBeat + **/ @Mutation() async lnBridgeHeartBeat( @Args('fromChainId') fromChainId: string, @@ -176,11 +222,13 @@ export class AggregationResolver { @Args('version') version: string, @Args('relayer') relayer: string, @Args('tokenAddress') tokenAddress: string, - @Args('softTransferLimit') softTransferLimit: string + @Args('softTransferLimit') softTransferLimit: string, ) { const id = `${version}-${fromChainId}-${toChainId}-${relayer.toLowerCase()}-${tokenAddress.toLowerCase()}`; + const now = Math.floor(Date.now() / 1000); + let updateData = { - heartbeatTimestamp: Math.floor(Date.now() / 1000), + heartbeatTimestamp: now, }; if (softTransferLimit !== undefined && softTransferLimit !== '0') { @@ -206,6 +254,104 @@ export class AggregationResolver { } } + @Mutation() + async signConfirmedBlock( + @Args('id') id: string, + @Args('relayer') relayer: string, + @Args('block') block: string, + @Args('timestamp') timestamp: number, + @Args('signature') signature: string + ) { + const allowSetConfirmed = this.checkMessageSender(timestamp, block, relayer?.toLowerCase(), signature); + if (!allowSetConfirmed) { + return; + } + await this.aggregationService.updateConfirmedBlock({ + where: { id: id }, + block: block, + }); + } + + @Mutation() + async signHeartBeat( + @Args('fromChainId') fromChainId: string, + @Args('toChainId') toChainId: string, + @Args('version') version: string, + @Args('relayer') relayer: string, + @Args('tokenAddress') tokenAddress: string, + @Args('softTransferLimit') softTransferLimit: string, + @Args('timestamp') timestamp: number, + @Args('signature') signature: string + ) { + const id = `${version}-${fromChainId}-${toChainId}-${relayer.toLowerCase()}-${tokenAddress.toLowerCase()}`; + const now = Math.floor(Date.now() / 1000); + + const allowHeartBeat = this.checkMessageSender(timestamp, softTransferLimit, relayer.toLowerCase(), signature); + if (!allowHeartBeat) { + return + } + + let updateData = { + heartbeatTimestamp: now, + }; + + if (softTransferLimit !== undefined && softTransferLimit !== '0') { + // the softTransferLimit is on target chain, transfer it to source chain + const transferLimit = this.aggregationService.targetAmountToSourceAmount({ + amount: softTransferLimit, + sourceChainId: Number(fromChainId), + targetChainId: Number(toChainId), + sourceToken: tokenAddress, + version + }); + updateData['softTransferLimit'] = transferLimit; + } + + try { + await this.aggregationService.updateLnBridgeRelayInfo({ + where: { id: id }, + data: updateData, + }); + } catch (e) { + console.log(`heart beat failed ${id}, exception: ${e}`); + return; + } + } + + @Mutation() + async signDynamicFee( + @Args('fromChainId') fromChainId: string, + @Args('toChainId') toChainId: string, + @Args('version') version: string, + @Args('relayer') relayer: string, + @Args('tokenAddress') tokenAddress: string, + @Args('dynamicFee') dynamicFee: string, + @Args('dynamicFeeExpire') dynamicFeeExpire: string, + @Args('dynamicFeeSignature') dynamicFeeSignature: string, + @Args('timestamp') timestamp: number, + @Args('signature') signature: string + ) { + const id = `${version}-${fromChainId}-${toChainId}-${relayer.toLowerCase()}-${tokenAddress.toLowerCase()}`; + const message = `${dynamicFee}:${dynamicFeeExpire}:${dynamicFeeSignature}`; + const allowSetDynamicFee = this.checkMessageSender(timestamp, message, relayer.toLowerCase(), signature); + if (!allowSetDynamicFee) { + return + } + + try { + await this.aggregationService.updateLnBridgeRelayInfo({ + where: { id: id }, + data: { + dynamicFee, + dynamicFeeExpire, + dynamicFeeSignature + }, + }); + } catch (e) { + return; + } + } + @Query() checkLnBridgeExist( @Args('fromChainId') fromChainId: number, diff --git a/apollo/src/graphql.ts b/apollo/src/graphql.ts index 66b44bae..1a6af1e2 100644 --- a/apollo/src/graphql.ts +++ b/apollo/src/graphql.ts @@ -118,6 +118,9 @@ export class LnBridgeRelayInfo { transferLimit?: Nullable; softTransferLimit?: Nullable; paused?: Nullable; + dynamicFee?: Nullable; + dynamicFeeExpire?: Nullable; + dynamicFeeSignature?: Nullable; } export class LnBridgeRelayInfos { @@ -146,6 +149,12 @@ export abstract class IMutation { abstract updateConfirmedBlock(id?: Nullable, block?: Nullable): Nullable | Promise>; abstract lnBridgeHeartBeat(fromChainId?: Nullable, toChainId?: Nullable, version?: Nullable, relayer?: Nullable, tokenAddress?: Nullable, softTransferLimit?: Nullable): Nullable | Promise>; + + abstract signConfirmedBlock(id?: Nullable, relayer?: Nullable, block?: Nullable, timestamp?: Nullable, signature?: Nullable): Nullable | Promise>; + + abstract signHeartBeat(fromChainId?: Nullable, toChainId?: Nullable, version?: Nullable, relayer?: Nullable, tokenAddress?: Nullable, softTransferLimit?: Nullable, timestamp?: Nullable, signature?: Nullable): Nullable | Promise>; + + abstract signDynamicFee(fromChainId?: Nullable, toChainId?: Nullable, version?: Nullable, relayer?: Nullable, tokenAddress?: Nullable, dynamicFee?: Nullable, dynamicFeeExpire?: Nullable, dynamicFeeSignature?: Nullable, timestamp?: Nullable, signature?: Nullable): Nullable | Promise>; } export type BigInt = any;