diff --git a/packages/service/gen/python/threecities/v1/transfer_verification_pb2.py b/packages/service/gen/python/threecities/v1/transfer_verification_pb2.py index 9a36dbd..aa0c657 100644 --- a/packages/service/gen/python/threecities/v1/transfer_verification_pb2.py +++ b/packages/service/gen/python/threecities/v1/transfer_verification_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*threecities/v1/transfer_verification.proto\x12\x0ethreecities.v1\"\xfe\x05\n\x1bTransferVerificationRequest\x12Q\n\x07trusted\x18\x01 \x01(\x0b\x32\x37.threecities.v1.TransferVerificationRequest.TrustedDataR\x07trusted\x12r\n\x18untrusted_to_be_verified\x18\x02 \x01(\x0b\x32\x39.threecities.v1.TransferVerificationRequest.UntrustedDataR\x15untrustedToBeVerified\x1a\xdc\x01\n\x0bTrustedData\x12\x1a\n\x08\x63urrency\x18\x01 \x01(\tR\x08\x63urrency\x12\x30\n\x14logical_asset_amount\x18\x02 \x01(\tR\x12logicalAssetAmount\x12\x34\n\x16token_ticker_allowlist\x18\x03 \x03(\tR\x14tokenTickerAllowlist\x12\x1e\n\x0busd_per_eth\x18\x04 \x01(\x01R\tusdPerEth\x12)\n\x10receiver_address\x18\x05 \x01(\tR\x0freceiverAddress\x1a\xef\x01\n\rUntrustedData\x12\x19\n\x08\x63hain_id\x18\x01 \x01(\rR\x07\x63hainId\x12)\n\x10transaction_hash\x18\x02 \x01(\tR\x0ftransactionHash\x12%\n\x0esender_address\x18\x03 \x01(\tR\rsenderAddress\x12q\n\x17\x63\x61ip222_style_signature\x18\x04 \x01(\x0b\x32\x39.threecities.v1.TransferVerificationRequest.SignatureDataR\x15\x63\x61ip222StyleSignature\x1aG\n\rSignatureData\x12\x18\n\x07message\x18\x01 \x01(\tR\x07message\x12\x1c\n\tsignature\x18\x02 \x01(\tR\tsignature\"w\n\x1cTransferVerificationResponse\x12\x1f\n\x0bis_verified\x18\x01 \x01(\x08R\nisVerified\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x14\n\x05\x65rror\x18\x03 \x01(\tR\x05\x65rror2\x90\x01\n\x1bTransferVerificationService\x12q\n\x14TransferVerification\x12+.threecities.v1.TransferVerificationRequest\x1a,.threecities.v1.TransferVerificationResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*threecities/v1/transfer_verification.proto\x12\x0ethreecities.v1\"\x9f\x06\n\x1bTransferVerificationRequest\x12Q\n\x07trusted\x18\x01 \x01(\x0b\x32\x37.threecities.v1.TransferVerificationRequest.TrustedDataR\x07trusted\x12r\n\x18untrusted_to_be_verified\x18\x02 \x01(\x0b\x32\x39.threecities.v1.TransferVerificationRequest.UntrustedDataR\x15untrustedToBeVerified\x1a\xfd\x01\n\x0bTrustedData\x12\x1a\n\x08\x63urrency\x18\x01 \x01(\tR\x08\x63urrency\x12\x30\n\x14logical_asset_amount\x18\x02 \x01(\tR\x12logicalAssetAmount\x12\x34\n\x16token_ticker_allowlist\x18\x03 \x03(\tR\x14tokenTickerAllowlist\x12\x1e\n\x0busd_per_eth\x18\x04 \x01(\x01R\tusdPerEth\x12)\n\x10receiver_address\x18\x05 \x01(\tR\x0freceiverAddress\x12\x1f\n\x0b\x65xternal_id\x18\x06 \x01(\tR\nexternalId\x1a\xef\x01\n\rUntrustedData\x12\x19\n\x08\x63hain_id\x18\x01 \x01(\rR\x07\x63hainId\x12)\n\x10transaction_hash\x18\x02 \x01(\tR\x0ftransactionHash\x12%\n\x0esender_address\x18\x03 \x01(\tR\rsenderAddress\x12q\n\x17\x63\x61ip222_style_signature\x18\x04 \x01(\x0b\x32\x39.threecities.v1.TransferVerificationRequest.SignatureDataR\x15\x63\x61ip222StyleSignature\x1aG\n\rSignatureData\x12\x18\n\x07message\x18\x01 \x01(\tR\x07message\x12\x1c\n\tsignature\x18\x02 \x01(\tR\tsignature\"\xe0\x01\n\x1cTransferVerificationResponse\x12\x1f\n\x0bis_verified\x18\x01 \x01(\x08R\nisVerified\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x14\n\x05\x65rror\x18\x03 \x01(\tR\x05\x65rror\x12\x1f\n\x0b\x65xternal_id\x18\x04 \x01(\tR\nexternalId\x12\x46\n\x1fverification_failed_permanently\x18\x05 \x01(\x08R\x1dverificationFailedPermanently2\x90\x01\n\x1bTransferVerificationService\x12q\n\x14TransferVerification\x12+.threecities.v1.TransferVerificationRequest\x1a,.threecities.v1.TransferVerificationResponseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -22,15 +22,15 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _globals['_TRANSFERVERIFICATIONREQUEST']._serialized_start=63 - _globals['_TRANSFERVERIFICATIONREQUEST']._serialized_end=829 + _globals['_TRANSFERVERIFICATIONREQUEST']._serialized_end=862 _globals['_TRANSFERVERIFICATIONREQUEST_TRUSTEDDATA']._serialized_start=294 - _globals['_TRANSFERVERIFICATIONREQUEST_TRUSTEDDATA']._serialized_end=514 - _globals['_TRANSFERVERIFICATIONREQUEST_UNTRUSTEDDATA']._serialized_start=517 - _globals['_TRANSFERVERIFICATIONREQUEST_UNTRUSTEDDATA']._serialized_end=756 - _globals['_TRANSFERVERIFICATIONREQUEST_SIGNATUREDATA']._serialized_start=758 - _globals['_TRANSFERVERIFICATIONREQUEST_SIGNATUREDATA']._serialized_end=829 - _globals['_TRANSFERVERIFICATIONRESPONSE']._serialized_start=831 - _globals['_TRANSFERVERIFICATIONRESPONSE']._serialized_end=950 - _globals['_TRANSFERVERIFICATIONSERVICE']._serialized_start=953 - _globals['_TRANSFERVERIFICATIONSERVICE']._serialized_end=1097 + _globals['_TRANSFERVERIFICATIONREQUEST_TRUSTEDDATA']._serialized_end=547 + _globals['_TRANSFERVERIFICATIONREQUEST_UNTRUSTEDDATA']._serialized_start=550 + _globals['_TRANSFERVERIFICATIONREQUEST_UNTRUSTEDDATA']._serialized_end=789 + _globals['_TRANSFERVERIFICATIONREQUEST_SIGNATUREDATA']._serialized_start=791 + _globals['_TRANSFERVERIFICATIONREQUEST_SIGNATUREDATA']._serialized_end=862 + _globals['_TRANSFERVERIFICATIONRESPONSE']._serialized_start=865 + _globals['_TRANSFERVERIFICATIONRESPONSE']._serialized_end=1089 + _globals['_TRANSFERVERIFICATIONSERVICE']._serialized_start=1092 + _globals['_TRANSFERVERIFICATIONSERVICE']._serialized_end=1236 # @@protoc_insertion_point(module_scope) diff --git a/packages/service/proto/threecities/v1/transfer_verification.proto b/packages/service/proto/threecities/v1/transfer_verification.proto index 9d189f1..3922a75 100644 --- a/packages/service/proto/threecities/v1/transfer_verification.proto +++ b/packages/service/proto/threecities/v1/transfer_verification.proto @@ -8,6 +8,7 @@ message TransferVerificationRequest { repeated string token_ticker_allowlist = 3; // allowlist of tokens to permit for a successfully verified transfer. WARNING today, not all supported tokens by 3cities are supported by 3cities on every chain (ie. the matrix of tokens * chains is incomplete), so if a ticker in tokenTickerAllowList is not available on the passed chainId, then any transfers of that token will not be detected and verification will fail as if the transfer never happened double usd_per_eth = 4; // ETH price in USD exchange rate to be used when verifying logical asset amounts string receiver_address = 5; // receiver address on the passed chainId where transfer being verified is expected to have been sent + string external_id = 6; // an optional external ID that may be provided by the client for tracking purposes. Not used by 3cities } message UntrustedData { // from the point of view of the verification client (caller), these data are untrusted and will be verified. Verification will be successful if and only if all these untrusted data are proven to be correct and match/correspond to the trusted data. NB as always, the RPC providers used by verification are assumed to be trustworthy - clients are trusting their RPC providers to facilitate verification @@ -31,6 +32,8 @@ message TransferVerificationResponse { bool is_verified = 1; // true iff the transfer verification was successful string description = 2; // description of verification result. Eg. if success, "0.023 ETH sent on Arbitrum One", if failure, "ChainID 3933 is not supported", "Insufficient confirmations, wanted=2, found=1" string error = 3; // optional error. Empty string indicates undefined. Always undefined if is_verified + string external_id = 4; // an optional external ID that may be provided by the client for tracking purposes. Not used by 3cities + bool verification_failed_permanently = 5; // true iff the verification is guaranteed to have failed permanently (eg. due to the transaction having reverted) and should not be retried. Must be ignored if is_verified } service TransferVerificationService { diff --git a/packages/service/src/connect.ts b/packages/service/src/connect.ts index eca7d08..10926c9 100644 --- a/packages/service/src/connect.ts +++ b/packages/service/src/connect.ts @@ -18,11 +18,15 @@ export default (router: ConnectRouter) => wagmiConfig, req, }); - return { + const resPb: TransferVerificationResponse = new TransferVerificationResponse({ isVerified: res.isVerified, description: res.description, ...(res.error && { error: res.error?.message } satisfies Pick), - }; + ...(res.externalId && { externalId: res.externalId } satisfies Pick), + ...(res.verificationFailedPermanently && { verificationFailedPermanently: res.verificationFailedPermanently } satisfies Pick), + }); + console.info(`req=${reqPb.toJsonString()} resp=${resPb.toJsonString()}`); + return resPb; } }, }); diff --git a/packages/service/src/fromProto.ts b/packages/service/src/fromProto.ts index 92be30d..4a46c04 100644 --- a/packages/service/src/fromProto.ts +++ b/packages/service/src/fromProto.ts @@ -76,6 +76,7 @@ export function transferVerificationRequestFromProto(pb: TransferVerificationReq })(); const req: TransferVerificationRequest = { trusted: { + ...(pb.trusted.externalId.length > 0 && { externalId: pb.trusted.externalId } satisfies Pick), currency, logicalAssetAmount, tokenTickerAllowlist: pb.trusted.tokenTickerAllowlist, diff --git a/packages/service/src/gen/threecities/v1/transfer_verification_pb.ts b/packages/service/src/gen/threecities/v1/transfer_verification_pb.ts index 1118792..4e0259f 100644 --- a/packages/service/src/gen/threecities/v1/transfer_verification_pb.ts +++ b/packages/service/src/gen/threecities/v1/transfer_verification_pb.ts @@ -90,6 +90,13 @@ export class TransferVerificationRequest_TrustedData extends Message) { super(); proto3.util.initPartial(data, this); @@ -103,6 +110,7 @@ export class TransferVerificationRequest_TrustedData extends Message): TransferVerificationRequest_TrustedData { @@ -257,6 +265,20 @@ export class TransferVerificationResponse extends Message) { super(); proto3.util.initPartial(data, this); @@ -268,6 +290,8 @@ export class TransferVerificationResponse extends Message): TransferVerificationResponse { diff --git a/packages/verifier/src/verifyTransfer.ts b/packages/verifier/src/verifyTransfer.ts index db4db38..0c3388f 100644 --- a/packages/verifier/src/verifyTransfer.ts +++ b/packages/verifier/src/verifyTransfer.ts @@ -1,4 +1,4 @@ -import { caip222StyleSignatureMessageDomain, caip222StyleSignatureMessagePrimaryType, caip222StyleSignatureMessageTypes, chainIdOnWhichToSignMessagesAndVerifySignatures, chainsSupportedBy3cities, convert, convertLogicalAssetUnits, erc1271MagicValue, erc1271SmartAccountAbi, getConfirmationsToWait, getLogicalAssetTickerForTokenOrNativeCurrencyTicker, getSupportedChainName, nativeCurrencies, tokens, type Caip222StyleMessageToSign, type Caip222StyleSignature, type NativeCurrency, type Token } from "@3cities/core"; +import { caip222StyleSignatureMessageDomain, caip222StyleSignatureMessagePrimaryType, caip222StyleSignatureMessageTypes, chainIdOnWhichToSignMessagesAndVerifySignatures, chainsSupportedBy3cities, convert, convertFromLogicalAssetDecimalsToTokenDecimals, erc1271MagicValue, erc1271SmartAccountAbi, getConfirmationsToWait, getLogicalAssetTickerForTokenOrNativeCurrencyTicker, getSupportedChainName, nativeCurrencies, tokens, type Caip222StyleMessageToSign, type Caip222StyleSignature, type NativeCurrency, type Token } from "@3cities/core"; import { ETHTransferProxyABI, getETHTransferProxyContractAddress } from "@3cities/eth-transfer-proxy"; import { getTransactionConfirmations, getTransactionReceipt, readContract, verifyTypedData, type Config } from "@wagmi/core"; import { erc20Abi, formatUnits, hashTypedData, isHex, parseEventLogs } from "viem"; @@ -11,6 +11,7 @@ export type TransferVerificationRequest = { tokenTickerAllowlist: string[]; // allowlist of tokens to permit for a successfully verified transfer. WARNING today, not all supported tokens by 3cities are supported by 3cities on every chain (ie. the matrix of tokens * chains is incomplete), so if a ticker in tokenTickerAllowList is not available on the passed chainId, then any transfers of that token will not be detected and verification will fail as if the transfer never happened. TODO response can include a note of which token tickers were not found on the passed chain id --> TODO should this be Set>? usdPerEth: number; // ETH price in USD exchange rate to be used when verifying logical asset amounts. TODO consider supporting undefined, in which case USD-to-ETH currency conversions can' be verified receiverAddress: `0x${string}`; // receiver address on the passed chainId where transfer being verified is expected to have been sent + externalId?: string; // an optional external ID that may be provided by the client for tracking purposes. Not used by 3cities }; untrustedToBeVerified: { // from the point of view of the verification client (caller), these data are untrusted and will be verified. Verification will be successful if and only if all these untrusted data are proven to be correct and match/correspond to the trusted data. NB as always, the RPC providers used by verification are assumed to be trustworthy - clients are trusting their RPC providers to facilitate verification chainId: number; // chainId on which the transfer is being verified @@ -27,19 +28,36 @@ type TransferVerificationResult = { isVerified: boolean; // true iff the transfer verification was successful description: string; // description of verification result. Eg. if success, "0.023 ETH sent on Arbitrum One", if failure, "ChainID 3933 is not supported", "Insufficient confirmations, wanted=2, found=1" error?: Error; + externalId?: string; // an optional external ID that may be provided by the client for tracking purposes. Not used by 3cities + verificationFailedPermanently?: boolean; // true iff the verification is guaranteed to have failed permanently (eg. due to the transaction having reverted) and should not be retried // TODO consider a failureReason enum/structured type to complement `description` // TODO consider a structured successData } & ({ isVerified: true; description: string; error?: never; + externalId?: string; + verificationFailedPermanently?: never; } | { isVerified: false; description: string; error?: Error; // TODO should Error be unconditionally defined when isVerified=false? probably? + externalId?: string; + verificationFailedPermanently: boolean; }); -export async function verifyTransfer({ wagmiConfig, req }: { +export async function verifyTransfer(params: { + wagmiConfig: Config, + req: TransferVerificationRequest, +}): Promise { + const res = await verifyTransferInternal(params); + return { + ...res, + ...(params.req.trusted.externalId && { externalId: params.req.trusted.externalId } satisfies Pick), + }; +} + +async function verifyTransferInternal({ wagmiConfig, req }: { wagmiConfig: Config, req: TransferVerificationRequest, }): Promise { @@ -60,12 +78,15 @@ export async function verifyTransfer({ wagmiConfig, req }: { isVerified: false, description: `Invalid request: sender address error`, error: senderAddressError, + verificationFailedPermanently: true, }; else if (chainsSupportedBy3cities.find(c => c.id === req.untrustedToBeVerified.chainId) === undefined) return { isVerified: false, description: `Chain ID ${req.untrustedToBeVerified.chainId} is unsupported by 3cities`, + verificationFailedPermanently: false, // NB verification has not necessarily failed permanently as 3cities might be update to support this chain ID }; else if (confirmationsToWait === undefined) return { isVerified: false, description: `Chain ID ${req.untrustedToBeVerified.chainId} had undefined confirmationsToWait. This is a bug in 3cities`, + verificationFailedPermanently: false, }; else { const getTransactionReceiptPromise = getTransactionReceipt(wagmiConfig, { hash: req.untrustedToBeVerified.transactionHash, chainId: req.untrustedToBeVerified.chainId }); const getTransactionConfirmationsPromise = getTransactionConfirmations(wagmiConfig, { hash: req.untrustedToBeVerified.transactionHash, chainId: req.untrustedToBeVerified.chainId }); @@ -132,30 +153,38 @@ export async function verifyTransfer({ wagmiConfig, req }: { isVerified: false, description: `Failed to get transaction receipt hash=${req.untrustedToBeVerified.transactionHash} chainId=${req.untrustedToBeVerified.chainId}`, error: getTransactionReceiptError, + verificationFailedPermanently: false, }; else if (getTransactionConfirmationsError) return { isVerified: false, description: `Failed to get transaction confirmations hash=${req.untrustedToBeVerified.transactionHash} chainId=${req.untrustedToBeVerified.chainId}`, + verificationFailedPermanently: false, error: getTransactionConfirmationsError, }; else if (tx.status !== 'success') return { isVerified: false, description: `Transaction reverted`, + verificationFailedPermanently: true, }; else if (txConfirmations < confirmationsToWait) return { isVerified: false, description: `Transaction has insufficient confirmations, wanted=${confirmationsToWait}, found=${txConfirmations}`, + verificationFailedPermanently: false, }; else if (verifyTypedDataError) return { isVerified: false, - description: `caip222-style non-eip1271 signature verification error`, + description: `caip222-style non-eip1271 signature verification error: ${verifyTypedDataError}`, error: verifyTypedDataError, + verificationFailedPermanently: false, }; else if (verifyTypedDataIsVerified === false) return { // NB verifyTypedDataIsVerified will be undefined if signature verification by this method is disabled isVerified: false, description: `caip222-style non-eip1271 signature verification failed`, + verificationFailedPermanently: true, }; else if (eip1271SignatureVerificationError) return { isVerified: false, - description: `caip222-style eip1271 signature verification error`, + description: `caip222-style eip1271 signature verification error: ${eip1271SignatureVerificationError}`, error: eip1271SignatureVerificationError, + verificationFailedPermanently: false, }; else if (eip1271SignatureIsVerified === false) return { // NB eip1271SignatureIsVerified will be undefined if signature verification by this method is disabled isVerified: false, description: `caip222-style eip1271 signature verification failed`, + verificationFailedPermanently: true, }; else { const ethTransferProxyContractAddress = getETHTransferProxyContractAddress(req.untrustedToBeVerified.chainId); // NB if ethTransferProxyContractAddress is undefined, then this chain does not support verifiable ETH transfers @@ -193,7 +222,7 @@ export async function verifyTransfer({ wagmiConfig, req }: { if (!nc) return [undefined, Error(`unexpected: native currency not found chainId=${req.untrustedToBeVerified.chainId}`)]; else { tempSuccessTokenForLog = nc; - return [convertLogicalAssetUnits(expectedUsdAmountInFullPrecisionLogicalAssetUnits, nc.decimals), undefined]; + return [convertFromLogicalAssetDecimalsToTokenDecimals(expectedUsdAmountInFullPrecisionLogicalAssetUnits, nc.decimals), undefined]; } } else { // erc20 transfer @@ -201,7 +230,7 @@ export async function verifyTransfer({ wagmiConfig, req }: { if (!t) return [undefined, Error(`unexpected: token not found when converting transfer amount decimals contractAddress=${l.address}`)]; else { tempSuccessTokenForLog = t; - return [convertLogicalAssetUnits(expectedUsdAmountInFullPrecisionLogicalAssetUnits, t.decimals), undefined]; + return [convertFromLogicalAssetDecimalsToTokenDecimals(expectedUsdAmountInFullPrecisionLogicalAssetUnits, t.decimals), undefined]; } } })(); @@ -254,11 +283,13 @@ export async function verifyTransfer({ wagmiConfig, req }: { if (logVerificationErrors.length > 0) return { isVerified: false, - description: `Transfer amount verification error(s)`, + description: `Transfer amount verification error(s): ${logVerificationErrors.map(e => e.message).join('; ')}`, error: new Error(`Transfer amount verification error(s): ${logVerificationErrors.map(e => e.message).join('; ')}`), + verificationFailedPermanently: true, }; else if (successfullyVerifiedLogs.length < 1) return { isVerified: false, description: `Transaction contained no satisfactory asset transfer`, + verificationFailedPermanently: true, }; else { const verificationSuccessReport = { senderAddress,