diff --git a/Cargo.lock b/Cargo.lock index 8027731e90..970859356f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1245,6 +1245,9 @@ name = "hex" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +dependencies = [ + "serde", +] [[package]] name = "hex-literal" diff --git a/api_tests/package.json b/api_tests/package.json index 1d96f06e4e..0e5d573e9b 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -22,6 +22,7 @@ "@types/find-cache-dir": "^3.2.0", "@types/glob": "^7.1.3", "@types/jest": "^26.0.10", + "@types/lodash": "^4.14.159", "@types/node": "^14.6", "@types/proper-lockfile": "^4.1.1", "@types/rimraf": "^3.0.0", @@ -40,6 +41,7 @@ "glob": "^7.1.6", "jasmine": "^3.6.1", "jest": "^26.4.1", + "lodash": "^4.17.20", "log4js": "^6.3.0", "p-timeout": "^3.2.0", "prettier": "^2.0.5", diff --git a/api_tests/src/actors/actor.ts b/api_tests/src/actors/actor.ts index 2d8d819082..4c3cfbf8d6 100644 --- a/api_tests/src/actors/actor.ts +++ b/api_tests/src/actors/actor.ts @@ -16,7 +16,7 @@ import { SwapStatus, } from "../payload"; import { Logger } from "log4js"; -import { E2ETestActorConfig } from "../config"; +import { CndConfigFile, E2ETestActorConfig } from "../config"; import { Asset, assetAsKey, @@ -34,6 +34,7 @@ import { defaultLedgerDescriptionForLedger, getIdentities } from "./defaults"; import pTimeout from "p-timeout"; import { Entity, Link } from "comit-sdk/dist/src/cnd/siren"; import { BtcDaiOrder } from "./order_factory"; +import { merge } from "lodash"; export type ActorName = "alice" | "bob" | "carol"; @@ -48,23 +49,25 @@ export class Actor { ledgerConfig: LedgerConfig, cargoTargetDirectory: string, cndLogFile: string, - logger: Logger + logger: Logger, + configOverrides: Partial ) { const actorConfig = await E2ETestActorConfig.for(name, logger); - const cndConfigFile = actorConfig.generateCndConfigFile(ledgerConfig); + const generatedConfig = actorConfig.generateCndConfigFile(ledgerConfig); + const finalConfig = merge(generatedConfig, configOverrides); const cndInstance = new CndInstance( cargoTargetDirectory, cndLogFile, logger, - cndConfigFile + finalConfig ); await cndInstance.start(); logger.info( "Created new actor with config %s", - JSON.stringify(cndConfigFile) + JSON.stringify(finalConfig) ); return new Actor(logger, cndInstance, name); diff --git a/api_tests/src/config.ts b/api_tests/src/config.ts index 66392fb8c4..8d8c342881 100644 --- a/api_tests/src/config.ts +++ b/api_tests/src/config.ts @@ -14,6 +14,9 @@ export interface CndConfigFile { data?: { dir: string }; network: { listen: string[] }; logging: { level: string }; + bitcoin?: BitcoinConfig; + ethereum?: EthereumConfig; + lightning?: LightningConfig; } export interface HttpApi { @@ -65,10 +68,8 @@ export class E2ETestActorConfig { }; } - private createLedgerConnectors( - ledgerConfig: LedgerConfig - ): LedgerConnectors { - const config: LedgerConnectors = {}; + private createLedgerConnectors(ledgerConfig: LedgerConfig): LedgerConfigs { + const config: LedgerConfigs = {}; if (ledgerConfig.bitcoin) { config.bitcoin = bitcoinConnector(ledgerConfig.bitcoin); @@ -106,26 +107,31 @@ export class E2ETestActorConfig { } } -interface LedgerConnectors { - bitcoin?: BitcoinConnector; - ethereum?: EthereumConnector; - lightning?: LightningConnector; +interface LedgerConfigs { + bitcoin?: BitcoinConfig; + ethereum?: EthereumConfig; + lightning?: LightningConfig; } interface Geth { node_url: string; } -interface EthereumConnector { +interface EthereumConfig { chain_id: number; geth: Geth; + tokens: Tokens; +} + +interface Tokens { + dai: string; } interface Bitcoind { node_url: string; } -interface BitcoinConnector { +interface BitcoinConfig { network: string; bitcoind: Bitcoind; } @@ -135,12 +141,12 @@ interface Lnd { dir: string; } -interface LightningConnector { +interface LightningConfig { network: string; lnd: Lnd; } -function bitcoinConnector(nodeConfig: BitcoinNodeConfig): BitcoinConnector { +function bitcoinConnector(nodeConfig: BitcoinNodeConfig): BitcoinConfig { return { bitcoind: { node_url: nodeConfig.rpcUrl, @@ -149,18 +155,19 @@ function bitcoinConnector(nodeConfig: BitcoinNodeConfig): BitcoinConnector { }; } -function ethereumConnector(nodeConfig: EthereumNodeConfig): EthereumConnector { +function ethereumConnector(nodeConfig: EthereumNodeConfig): EthereumConfig { return { chain_id: nodeConfig.chain_id, geth: { node_url: nodeConfig.rpc_url, }, + tokens: { + dai: nodeConfig.tokenContract, + }, }; } -function lightningConnector( - nodeConfig: LightningNodeConfig -): LightningConnector { +function lightningConnector(nodeConfig: LightningNodeConfig): LightningConfig { return { network: "regtest", lnd: { diff --git a/api_tests/src/create_actors.ts b/api_tests/src/create_actors.ts index 8e23504dfb..03c3a0bd45 100644 --- a/api_tests/src/create_actors.ts +++ b/api_tests/src/create_actors.ts @@ -21,7 +21,8 @@ export async function createActors( global.ledgerConfigs, global.cargoTargetDir, cndLogFile, - actorLogger + actorLogger, + global.cndConfigOverrides ) ); } diff --git a/api_tests/src/test_environment.ts b/api_tests/src/test_environment.ts index 3314db4372..ad108f5ee7 100644 --- a/api_tests/src/test_environment.ts +++ b/api_tests/src/test_environment.ts @@ -17,6 +17,8 @@ import { LedgerInstance, LightningNodeConfig } from "./ledgers"; import { GethInstance } from "./ledgers/geth_instance"; import { LndInstance } from "./ledgers/lnd_instance"; import BitcoinRpcClient from "bitcoin-core"; +import { CndConfigFile } from "./config"; +import { set } from "lodash"; export default class TestEnvironment extends NodeEnvironment { private readonly testSuite: string; @@ -25,6 +27,7 @@ export default class TestEnvironment extends NodeEnvironment { private readonly locksDir: string; private readonly nodeModulesBinDir: string; private readonly srcDir: string; + private readonly cndConfigOverrides: Partial; public global: HarnessGlobal; @@ -33,9 +36,12 @@ export default class TestEnvironment extends NodeEnvironment { constructor(config: Config.ProjectConfig, context: EnvironmentContext) { super(config); - this.ledgers = TestEnvironment.extractLedgersToBeStarted( + this.ledgers = extractLedgersToBeStarted(context.docblockPragmas); + this.cndConfigOverrides = extractCndConfigOverrides( context.docblockPragmas ); + assertNoUnhandledPargmas(context.docblockPragmas); + this.logDir = path.resolve(config.rootDir, "log"); this.locksDir = path.resolve(config.rootDir, "locks"); this.nodeModulesBinDir = path.resolve( @@ -60,6 +66,7 @@ export default class TestEnvironment extends NodeEnvironment { this.global.ledgerConfigs = {}; this.global.lndWallets = {}; this.global.cargoTargetDir = cargoTargetDir; + this.global.cndConfigOverrides = this.cndConfigOverrides; const log4js = configure({ appenders: { @@ -389,20 +396,54 @@ export default class TestEnvironment extends NodeEnvironment { return dir; } +} - private static extractLedgersToBeStarted( - docblockPragmas: Record - ): string[] { - const ledgersToStart = docblockPragmas.ledger; +function extractLedgersToBeStarted( + docblockPragmas: Record +): string[] { + const ledgersToStart = docblockPragmas.ledger; + delete docblockPragmas.ledger; - if (!ledgersToStart) { - return []; - } + if (!ledgersToStart) { + return []; + } - if (typeof ledgersToStart === "string") { - return [ledgersToStart]; - } + if (typeof ledgersToStart === "string") { + return [ledgersToStart]; + } + + return ledgersToStart; +} + +export function extractCndConfigOverrides( + docblockPragmas: Record +): Partial { + let configOverrides = docblockPragmas.cndConfigOverride; + delete docblockPragmas.cndConfigOverride; + + if (!configOverrides) { + return {}; + } + + // generalize single override to list of overrides + if (typeof configOverrides === "string") { + configOverrides = [configOverrides]; + } + + return configOverrides + .map((override) => override.split(" = ")) + .filter(([key, _]) => key !== "") + .reduce((config, [key, value]) => { + set(config, key, value); + + return config; + }, {}); +} - return ledgersToStart; +export function assertNoUnhandledPargmas( + docblockPragmas: Record +) { + for (const [pragma] of Object.entries(docblockPragmas)) { + throw new Error(`Unhandled pragma '${pragma}'! Typo?`); } } diff --git a/api_tests/src/utils.ts b/api_tests/src/utils.ts index 263cee03fc..b2e47b7079 100644 --- a/api_tests/src/utils.ts +++ b/api_tests/src/utils.ts @@ -11,6 +11,7 @@ import { EthereumNodeConfig, LightningNodeConfig, } from "./ledgers"; +import { CndConfigFile } from "./config"; export interface HarnessGlobal extends Global.Global { ledgerConfigs: LedgerConfig; @@ -21,6 +22,7 @@ export interface HarnessGlobal extends Global.Global { tokenContract: string; gethLockDir: string; cargoTargetDir: string; + cndConfigOverrides: Partial; getDataDir: (program: string) => Promise; getLogFile: (pathElements: string[]) => string; diff --git a/api_tests/tests/sanity.ts b/api_tests/tests/sanity.ts index b106da94e5..f83e01d575 100644 --- a/api_tests/tests/sanity.ts +++ b/api_tests/tests/sanity.ts @@ -1,3 +1,8 @@ +/** + * @cndConfigOverride ethereum.chain_id = 1337 + * @cndConfigOverride ethereum.tokens.dai = 0x0000000000000000000000000000000000000000 + */ + import { oneActorTest, twoActorTest } from "../src/actor_test"; import SwapFactory from "../src/actors/swap_factory"; diff --git a/api_tests/tests/test_environment.spec.ts b/api_tests/tests/test_environment.spec.ts new file mode 100644 index 0000000000..70e2917fb0 --- /dev/null +++ b/api_tests/tests/test_environment.spec.ts @@ -0,0 +1,61 @@ +import { extractCndConfigOverrides } from "../src/test_environment"; + +describe("extractCndConfigOverrides", () => { + test("given no overrides, returns empty object", () => { + expect(extractCndConfigOverrides({})).toStrictEqual({}); + expect( + extractCndConfigOverrides({ + cndConfigOverride: null, + }) + ).toStrictEqual({}); + expect( + extractCndConfigOverrides({ + cndConfigOverride: "", + }) + ).toStrictEqual({}); + expect( + extractCndConfigOverrides({ + cndConfigOverride: ["", "", ""], + }) + ).toStrictEqual({}); + }); + + test("given overrides, sets nested key", () => { + const overrides = { + cndConfigOverride: + "ethereum.tokens.dai = 0x0000000000000000000000000000000000000000", + }; + + const config = extractCndConfigOverrides(overrides); + + expect(config).toStrictEqual({ + ethereum: { + tokens: { + dai: "0x0000000000000000000000000000000000000000", + }, + }, + }); + }); + + test("given two overrides, sets them both", () => { + const overrides = { + cndConfigOverride: [ + "ethereum.tokens.dai = 0x0000000000000000000000000000000000000000", + "bitcoin.network = regtest", + ], + }; + + const config = extractCndConfigOverrides(overrides); + + expect(config).toStrictEqual({ + ethereum: { + tokens: { + dai: "0x0000000000000000000000000000000000000000", + }, + }, + bitcoin: { + network: "regtest", + }, + }); + }); +}); diff --git a/api_tests/yarn.lock b/api_tests/yarn.lock index 90eefa142f..d8f25ec602 100644 --- a/api_tests/yarn.lock +++ b/api_tests/yarn.lock @@ -1073,6 +1073,11 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" +"@types/lodash@^4.14.159": + version "4.14.159" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" + integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== + "@types/long@*", "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -4211,6 +4216,11 @@ lodash@^4.0.0, lodash@^4.17.13, lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== +lodash@^4.17.20: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + log4js@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb" diff --git a/cnd/src/config.rs b/cnd/src/config.rs index 294319f87e..6bc1fa92d2 100644 --- a/cnd/src/config.rs +++ b/cnd/src/config.rs @@ -3,7 +3,7 @@ mod serde_bitcoin_network; mod settings; mod validation; -use crate::{ethereum::ChainId, fs}; +use crate::{ethereum, ethereum::ChainId, fs}; use anyhow::{Context, Result}; use conquer_once::Lazy; use libp2p::Multiaddr; @@ -25,6 +25,24 @@ static LND_URL: Lazy = Lazy::new(|| parse_unchecked("https://localhost:8080 static WEB3_URL: Lazy = Lazy::new(|| parse_unchecked("http://localhost:8545")); +/// The DAI token contract on Ethereum mainnet. +/// +/// Source: https://changelog.makerdao.com/ +static DAI_MAINNET: Lazy = + Lazy::new(|| parse_unchecked("0x6B175474E89094C44Da98b954EedeAC495271d0F")); + +/// The DAI token contract on the Ethereum testnet "kovan". +/// +/// Source: https://changelog.makerdao.com/ +static DAI_KOVAN: Lazy = + Lazy::new(|| parse_unchecked("0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa")); + +/// The DAI token contract on the Ethereum testnet "ropsten". +/// +/// Source: https://changelog.makerdao.com/ +static DAI_ROPSTEN: Lazy = + Lazy::new(|| parse_unchecked("0x31F42841c2db5173425b5223809CF3A38FEde360")); + static COMIT_SOCKET: Lazy = Lazy::new(|| parse_unchecked("/ip4/0.0.0.0/tcp/9939")); #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -118,14 +136,16 @@ impl From for file::Bitcoin { pub struct Ethereum { pub chain_id: ChainId, pub geth: Geth, + pub tokens: Tokens, } impl Ethereum { - fn new(chain_id: ChainId) -> Self { - Self { + fn new(chain_id: ChainId) -> Result { + Ok(Self { chain_id, geth: Geth::new(), - } + tokens: Tokens::new(chain_id)?, + }) } fn from_file(ethereum: file::Ethereum, comit_network: Option) -> Result { @@ -143,8 +163,16 @@ impl Ethereum { let chain_id = ethereum.chain_id; let geth = ethereum.geth.unwrap_or_else(Geth::new); + let tokens = ethereum.tokens.map_or_else( + || Tokens::new(chain_id), + |file| Tokens::from_file(file, chain_id), + )?; - Ok(Ethereum { chain_id, geth }) + Ok(Ethereum { + chain_id, + geth, + tokens, + }) } } @@ -153,6 +181,15 @@ impl From for file::Ethereum { file::Ethereum { chain_id: ethereum.chain_id, geth: Some(ethereum.geth), + tokens: Some(ethereum.tokens.into()), + } + } +} + +impl From for file::Tokens { + fn from(tokens: Tokens) -> Self { + file::Tokens { + dai: Some(tokens.dai), } } } @@ -170,6 +207,37 @@ impl Geth { } } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Tokens { + pub dai: ethereum::Address, +} + +impl Tokens { + fn new(chain_id: ChainId) -> Result { + let dai = dai_address_from_chain_id(chain_id)?; + + Ok(Self { dai }) + } + + fn from_file(file: file::Tokens, id: ChainId) -> Result { + let dai = file.dai.map_or_else(|| dai_address_from_chain_id(id), Ok)?; + + Ok(Self { dai }) + } +} + +fn dai_address_from_chain_id(id: ChainId) -> Result { + Ok(match id { + ChainId::MAINNET => *DAI_MAINNET, + ChainId::ROPSTEN => *DAI_ROPSTEN, + ChainId::KOVAN => *DAI_KOVAN, + id => anyhow::bail!( + "unable to infer DAI token contract from chain-ID {}", + u32::from(id) + ), + }) +} + #[derive(Clone, Debug, PartialEq, Serialize)] pub struct Lightning { pub network: bitcoin::Network, diff --git a/cnd/src/config/file.rs b/cnd/src/config/file.rs index 4bdce07f7b..2486833ee8 100644 --- a/cnd/src/config/file.rs +++ b/cnd/src/config/file.rs @@ -1,5 +1,6 @@ use crate::{ config::{Bitcoind, Data, Geth, Network}, + ethereum, ethereum::ChainId, }; use log::LevelFilter; @@ -37,6 +38,12 @@ pub struct Bitcoin { pub struct Ethereum { pub chain_id: ChainId, pub geth: Option, + pub tokens: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Tokens { + pub dai: Option, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -230,6 +237,9 @@ chain_id = 1337 [ethereum.geth] node_url = "http://localhost:8545/" +[ethereum.tokens] +dai = "0x6b175474e89094c44da98b954eedeac495271d0f" + [lightning] network = "regtest" @@ -264,6 +274,13 @@ dir = "/foo/bar" geth: Some(Geth { node_url: "http://localhost:8545".parse().unwrap(), }), + tokens: Some(Tokens { + dai: Some( + "0x6b175474e89094c44da98b954eedeac495271d0f" + .parse() + .unwrap(), + ), + }), }), lightning: Some(Lightning { network: bitcoin::Network::Regtest, @@ -350,40 +367,67 @@ dir = "/foo/bar" fn ethereum_deserializes_correctly() { let file_contents = vec![ r#" - chain_id = 1337 + chain_id = 42 [geth] node_url = "http://example.com:8545" + [tokens] + dai = "0xc4375b7de8af5a38a93548eb8453a498222c4ff2" "#, r#" chain_id = 3 [geth] node_url = "http://example.com:8545" + [tokens] + dai = "0xaD6D458402F60fD3Bd25163575031ACDce07538D" "#, r#" chain_id = 1 [geth] node_url = "http://example.com:8545" + [tokens] + dai = "0x6b175474e89094c44da98b954eedeac495271d0f" "#, ]; let expected = vec![ Ethereum { - chain_id: ChainId::GETH_DEV, + chain_id: ChainId::KOVAN, geth: Some(Geth { node_url: Url::parse("http://example.com:8545").unwrap(), }), + tokens: Some(Tokens { + dai: Some( + "0xc4375b7de8af5a38a93548eb8453a498222c4ff2" + .parse() + .unwrap(), + ), + }), }, Ethereum { chain_id: ChainId::ROPSTEN, geth: Some(Geth { node_url: Url::parse("http://example.com:8545").unwrap(), }), + tokens: Some(Tokens { + dai: Some( + "0xaD6D458402F60fD3Bd25163575031ACDce07538D" + .parse() + .unwrap(), + ), + }), }, Ethereum { chain_id: ChainId::MAINNET, geth: Some(Geth { node_url: Url::parse("http://example.com:8545").unwrap(), }), + tokens: Some(Tokens { + dai: Some( + "0x6b175474e89094c44da98b954eedeac495271d0f" + .parse() + .unwrap(), + ), + }), }, ]; diff --git a/cnd/src/config/settings.rs b/cnd/src/config/settings.rs index 89e743d204..5fcb406c81 100644 --- a/cnd/src/config/settings.rs +++ b/cnd/src/config/settings.rs @@ -154,7 +154,7 @@ impl Settings { |file| Bitcoin::from_file(file, comit_network), )?, ethereum: ethereum.map_or_else( - || Ok(Ethereum::new(comit_network.unwrap_or_default().into())), + || Ethereum::new(comit_network.unwrap_or_default().into()), |file| Ethereum::from_file(file, comit_network), )?, lightning: lightning.map_or_else( @@ -169,7 +169,7 @@ impl Settings { mod tests { use super::*; use crate::{ - config::{file, Bitcoind, Geth, Lnd}, + config::{file, Bitcoind, Geth, Lnd, Tokens, DAI_MAINNET}, ethereum::ChainId, }; use spectral::prelude::*; @@ -311,6 +311,7 @@ mod tests { geth: Geth { node_url: "http://localhost:8545".parse().unwrap(), }, + tokens: Tokens { dai: *DAI_MAINNET }, }) } diff --git a/cnd/src/http_api/halbit_herc20.rs b/cnd/src/http_api/halbit_herc20.rs index 288fddab50..2af3f37e4a 100644 --- a/cnd/src/http_api/halbit_herc20.rs +++ b/cnd/src/http_api/halbit_herc20.rs @@ -8,16 +8,13 @@ use crate::{ storage::Save, Facade, LocalSwapId, }; -use serde::Deserialize; use warp::{http::StatusCode, Rejection, Reply}; #[allow(clippy::needless_pass_by_value)] -pub async fn post_swap(body: serde_json::Value, facade: Facade) -> Result { - let body = PostBody::::deserialize(&body) - .map_err(anyhow::Error::new) - .map_err(problem::from_anyhow) - .map_err(warp::reject::custom)?; - +pub async fn post_swap( + body: PostBody, + facade: Facade, +) -> Result { let swap_id = LocalSwapId::default(); let reply = warp::reply::reply(); diff --git a/cnd/src/http_api/hbit_herc20.rs b/cnd/src/http_api/hbit_herc20.rs index d2235e4c74..6341cf16f3 100644 --- a/cnd/src/http_api/hbit_herc20.rs +++ b/cnd/src/http_api/hbit_herc20.rs @@ -8,16 +8,13 @@ use crate::{ storage::Save, Facade, LocalSwapId, Side, }; -use serde::Deserialize; use warp::{http::StatusCode, Rejection, Reply}; #[allow(clippy::needless_pass_by_value)] -pub async fn post_swap(body: serde_json::Value, facade: Facade) -> Result { - let body = PostBody::::deserialize(&body) - .map_err(anyhow::Error::new) - .map_err(problem::from_anyhow) - .map_err(warp::reject::custom)?; - +pub async fn post_swap( + body: PostBody, + facade: Facade, +) -> Result { let swap_id = LocalSwapId::default(); let reply = warp::reply::reply(); diff --git a/cnd/src/http_api/herc20_halbit.rs b/cnd/src/http_api/herc20_halbit.rs index 56790d4382..696c09abbc 100644 --- a/cnd/src/http_api/herc20_halbit.rs +++ b/cnd/src/http_api/herc20_halbit.rs @@ -8,15 +8,12 @@ use crate::{ storage::Save, Facade, LocalSwapId, }; -use serde::Deserialize; use warp::{http::StatusCode, Rejection, Reply}; -pub async fn post_swap(body: serde_json::Value, facade: Facade) -> Result { - let body = PostBody::::deserialize(&body) - .map_err(anyhow::Error::new) - .map_err(problem::from_anyhow) - .map_err(warp::reject::custom)?; - +pub async fn post_swap( + body: PostBody, + facade: Facade, +) -> Result { let swap_id = LocalSwapId::default(); let reply = warp::reply::reply(); diff --git a/cnd/src/http_api/herc20_hbit.rs b/cnd/src/http_api/herc20_hbit.rs index 34e93f5f7b..955f9538e8 100644 --- a/cnd/src/http_api/herc20_hbit.rs +++ b/cnd/src/http_api/herc20_hbit.rs @@ -9,16 +9,13 @@ use crate::{ Facade, LocalSwapId, Side, }; use comit::network::swap_digest; -use serde::Deserialize; use warp::{http::StatusCode, Rejection, Reply}; #[allow(clippy::needless_pass_by_value)] -pub async fn post_swap(body: serde_json::Value, facade: Facade) -> Result { - let body = PostBody::::deserialize(&body) - .map_err(anyhow::Error::new) - .map_err(problem::from_anyhow) - .map_err(warp::reject::custom)?; - +pub async fn post_swap( + body: PostBody, + facade: Facade, +) -> Result { let swap_id = LocalSwapId::default(); let reply = warp::reply::reply(); diff --git a/cnd/src/http_api/orderbook.rs b/cnd/src/http_api/orderbook.rs index 359f538d54..291ca97394 100644 --- a/cnd/src/http_api/orderbook.rs +++ b/cnd/src/http_api/orderbook.rs @@ -14,14 +14,9 @@ use warp::{http, http::StatusCode, Rejection, Reply}; pub async fn post_take_order( order_id: OrderId, - body: serde_json::Value, + body: TakeOrderBody, mut facade: Facade, ) -> Result { - let body = TakeOrderBody::deserialize(&body) - .map_err(anyhow::Error::new) - .map_err(problem::from_anyhow) - .map_err(warp::reject::custom)?; - let reply = warp::reply::reply(); let swap_id = LocalSwapId::default(); @@ -116,15 +111,7 @@ pub async fn post_take_order( .map_err(warp::reject::custom) } -pub async fn post_make_order( - body: serde_json::Value, - facade: Facade, -) -> Result { - let body = MakeOrderBody::deserialize(&body) - .map_err(anyhow::Error::new) - .map_err(problem::from_anyhow) - .map_err(warp::reject::custom)?; - +pub async fn post_make_order(body: MakeOrderBody, facade: Facade) -> Result { let reply = warp::reply::reply(); let order = NewOrder::from(body.clone()); @@ -218,7 +205,7 @@ pub async fn get_orders(facade: Facade) -> Result { } #[derive(Clone, Debug, Deserialize)] -struct MakeOrderBody { +pub struct MakeOrderBody { position: Position, #[serde(with = "asset::bitcoin::sats_as_string")] bitcoin_amount: asset::Bitcoin, @@ -248,7 +235,7 @@ impl From for NewOrder { } #[derive(Clone, Debug, Deserialize)] -struct TakeOrderBody { +pub struct TakeOrderBody { ethereum_identity: identity::Ethereum, bitcoin_identity: bitcoin::Address, } diff --git a/cnd/src/http_api/problem.rs b/cnd/src/http_api/problem.rs index f7e458097e..1cbf7099aa 100644 --- a/cnd/src/http_api/problem.rs +++ b/cnd/src/http_api/problem.rs @@ -1,6 +1,7 @@ use crate::{http_api::ActionNotFound, storage::NoSwapExists}; use http_api_problem::HttpApiProblem; use warp::{ + body::BodyDeserializeError, http::{self, StatusCode}, Rejection, Reply, }; @@ -22,14 +23,6 @@ pub fn from_anyhow(e: anyhow::Error) -> HttpApiProblem { return HttpApiProblem::new("Swap not found.").set_status(StatusCode::NOT_FOUND); } - if e.is::() { - tracing::error!("deserialization error: {}", e); - - return HttpApiProblem::new("Invalid body.") - .set_status(StatusCode::BAD_REQUEST) - .set_detail(format!("{:?}", e)); - } - if e.is::() { return HttpApiProblem::new("Action not found.").set_status(StatusCode::NOT_FOUND); } @@ -49,18 +42,29 @@ pub fn from_anyhow(e: anyhow::Error) -> HttpApiProblem { pub async fn unpack_problem(rejection: Rejection) -> Result { if let Some(problem) = rejection.find::() { - let code = problem.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); - - let reply = warp::reply::json(problem); - let reply = warp::reply::with_status(reply, code); - let reply = warp::reply::with_header( - reply, - http::header::CONTENT_TYPE, - http_api_problem::PROBLEM_JSON_MEDIA_TYPE, - ); + return Ok(problem_to_reply(problem)); + } - return Ok(reply); + if let Some(invalid_body) = rejection.find::() { + return Ok(problem_to_reply( + &HttpApiProblem::new("Invalid body.") + .set_status(StatusCode::BAD_REQUEST) + .set_detail(format!("{:?}", invalid_body)), + )); } Err(rejection) } + +fn problem_to_reply(problem: &HttpApiProblem) -> impl Reply { + let code = problem.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + + let reply = warp::reply::json(problem); + let reply = warp::reply::with_status(reply, code); + + warp::reply::with_header( + reply, + http::header::CONTENT_TYPE, + http_api_problem::PROBLEM_JSON_MEDIA_TYPE, + ) +} diff --git a/comit/Cargo.toml b/comit/Cargo.toml index 58b879386f..ef95c69321 100644 --- a/comit/Cargo.toml +++ b/comit/Cargo.toml @@ -19,7 +19,7 @@ digest = { path = "../digest" } ethbloom = "0.9.1" futures = { version = "0.3", default-features = false } genawaiter = { version = "0.99", default-features = false, features = ["futures03"] } -hex = "0.4" +hex = { version = "0.4", features = ["serde"] } levenshtein = "1" libp2p = { version = "0.23", default-features = false, features = ["gossipsub", "request-response"] } lru = "0.6.0" diff --git a/comit/src/btsieve/ethereum.rs b/comit/src/btsieve/ethereum.rs index 37772be2e7..1b66f7a0dc 100644 --- a/comit/src/btsieve/ethereum.rs +++ b/comit/src/btsieve/ethereum.rs @@ -165,7 +165,7 @@ where if matcher(&transaction) { let receipt = fetch_receipt(connector, tx_hash).await?; - if !receipt.is_status_ok() { + if !receipt.successful { // This can be caused by a failed attempt to complete an action, // for example, sending a transaction with low gas. tracing::warn!("transaction matched but status was NOT OK"); @@ -235,9 +235,9 @@ where let _enter = span.enter(); let receipt = fetch_receipt(connector, tx_hash).await?; - let status_is_ok = receipt.is_status_ok(); + let is_successful = receipt.successful; if let Some(log) = matcher(receipt) { - if !status_is_ok { + if !is_successful { // This can be caused by a failed attempt to complete an action, // for example, sending a transaction with low gas. tracing::warn!("transaction matched but status was NOT OK"); diff --git a/comit/src/ethereum.rs b/comit/src/ethereum.rs index 77caa187a2..9fa3f96df9 100644 --- a/comit/src/ethereum.rs +++ b/comit/src/ethereum.rs @@ -1,8 +1,8 @@ use crate::{btsieve::LatestBlock, Timestamp}; pub use ethbloom::{Bloom as H2048, Input}; +use hex::FromHexError; pub use primitive_types::U256; -use serde::{Deserialize, Serialize}; -use serde_hex::{CompactPfx, SerHex, SerHexSeq, StrictPfx}; +use serde::{Deserialize, Deserializer, Serialize}; use std::{ fmt, fmt::{Display, Formatter}, @@ -21,7 +21,7 @@ where #[derive( Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] -pub struct Address(#[serde(with = "SerHex::")] [u8; 20]); +pub struct Address(#[serde(with = "serde_hex_data")] [u8; 20]); impl Address { pub fn as_bytes(&self) -> &[u8; 20] { @@ -53,23 +53,11 @@ impl From
for [u8; 20] { } impl FromStr for Address { - type Err = FromHexStrError; + type Err = FromHexError; fn from_str(hex: &str) -> Result { - let bytes = hex::decode(hex.trim_start_matches("0x"))?; - - const EXPECTED_LEN: usize = 20; - let len = bytes.len(); - - if len != EXPECTED_LEN { - return Err(FromHexStrError::InvalidLength { - expected: EXPECTED_LEN, - got: len, - }); - } - - let mut address = [0u8; EXPECTED_LEN]; - address.copy_from_slice(&bytes); + let mut address = [0u8; 20]; + hex::decode_to_slice(hex.trim_start_matches("0x"), &mut address)?; Ok(Address(address)) } @@ -96,7 +84,7 @@ impl From
for Hash { #[derive( Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] -pub struct Hash(#[serde(with = "SerHex::")] [u8; 32]); +pub struct Hash(#[serde(with = "serde_hex_data")] [u8; 32]); impl From<[u8; 32]> for Hash { fn from(bytes: [u8; 32]) -> Self { @@ -127,36 +115,16 @@ impl Display for Hash { } impl FromStr for Hash { - type Err = FromHexStrError; + type Err = FromHexError; fn from_str(hex: &str) -> Result { - let bytes = hex::decode(hex.trim_start_matches("0x"))?; - - const EXPECTED_LEN: usize = 32; - let len = bytes.len(); - - if len != EXPECTED_LEN { - return Err(FromHexStrError::InvalidLength { - expected: EXPECTED_LEN, - got: len, - }); - } - - let mut hash = [0u8; EXPECTED_LEN]; - hash.copy_from_slice(&bytes); + let mut hash = [0u8; 32]; + hex::decode_to_slice(hex.trim_start_matches("0x"), &mut hash)?; Ok(Hash(hash)) } } -#[derive(Debug, Clone, Copy, thiserror::Error)] -pub enum FromHexStrError { - #[error("unable to decode string as hex")] - InvalidHex(#[from] hex::FromHexError), - #[error("expected a hex string with {expected} bytes but got {got} bytes")] - InvalidLength { expected: usize, got: usize }, -} - /// "Receipt" of an executed transaction: details of its execution. #[derive(Debug, Default, Clone, PartialEq, Deserialize)] pub struct TransactionReceipt { @@ -165,15 +133,18 @@ pub struct TransactionReceipt { pub contract_address: Option
, /// Logs generated within this transaction. pub logs: Vec, - /// Status: either 1 (success) or 0 (failure). - #[serde(with = "SerHex::")] - pub status: u8, + /// Status: Whether or not the transaction executed successfully + #[serde(rename = "status", deserialize_with = "deserialize_status")] + pub successful: bool, } -impl TransactionReceipt { - pub fn is_status_ok(&self) -> bool { - self.status == 1 - } +fn deserialize_status<'de, D>(deserializer: D) -> Result>::Error> +where + D: Deserializer<'de>, +{ + let hex_string = String::deserialize(deserializer)?; + + Ok(&hex_string == "0x1") } /// Description of a Transaction, pending or in the chain. @@ -186,7 +157,7 @@ pub struct Transaction { /// Transfered value pub value: U256, /// Input data - #[serde(with = "SerHexSeq::")] + #[serde(with = "serde_hex_data")] pub input: Vec, } @@ -198,7 +169,7 @@ pub struct Log { /// Topics pub topics: Vec, /// Data - #[serde(with = "SerHexSeq::")] + #[serde(with = "serde_hex_data")] pub data: Vec, } @@ -227,6 +198,7 @@ pub struct ChainId(u32); impl ChainId { pub const MAINNET: Self = ChainId(1); pub const ROPSTEN: Self = ChainId(3); + pub const KOVAN: Self = ChainId(42); pub const GETH_DEV: Self = ChainId(1337); } @@ -253,6 +225,36 @@ impl From for ChainId { } } +/// A serde module for formatting bytes according to Ethereum's convention for +/// "data". +/// +/// See https://eth.wiki/json-rpc/API#hex-value-encoding for more details. +pub mod serde_hex_data { + use super::*; + use hex::FromHex; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &V, serializer: S) -> Result + where + S: Serializer, + V: AsRef<[u8]>, + { + serializer.serialize_str(&format!("0x{}", hex::encode(value.as_ref()))) + } + + pub fn deserialize<'de, D, V>(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + V: FromHex, + ::Error: Display, + { + let string = String::deserialize(deserializer)?; + let value = V::from_hex(string.trim_start_matches("0x")).map_err(D::Error::custom)?; + + Ok(value) + } +} + #[cfg(test)] mod tests { use super::*; @@ -266,17 +268,6 @@ mod tests { let _: Address = Address::deserialize(&json).unwrap(); } - #[test] - fn deserialise_address_when_not_using_reference_to_deserialize_fails() { - // This is due to a bug in serde-jex, keep this test until https://github.com/fspmarshall/serde-hex/pull/8 - // is fixed. - let json = - serde_json::Value::String("0xc5549e335b2786520f4c5d706c76c9ee69d0a028".to_owned()); - - let deserialized = serde_json::from_value::
(json); - matches!(deserialized, Err(_)); - } - #[test] fn from_string_address() { let json = @@ -342,7 +333,7 @@ mod tests { let receipt = serde_json::from_str::(json).unwrap(); - assert_eq!(receipt.status, 1); + assert_eq!(receipt.successful, true); } #[test] @@ -357,7 +348,7 @@ mod tests { let receipt = serde_json::from_str::(json).unwrap(); - assert_eq!(receipt.status, 0); + assert_eq!(receipt.successful, false); } proptest! { diff --git a/comit/src/network/protocols/bitcoin_identity.rs b/comit/src/network/protocols/bitcoin_identity.rs index 9232619f3e..6427f45301 100644 --- a/comit/src/network/protocols/bitcoin_identity.rs +++ b/comit/src/network/protocols/bitcoin_identity.rs @@ -9,6 +9,7 @@ pub struct Message { pub swap_id: SharedSwapId, /// A compressed Bitcoin public key, serialized as hex without a `0x` prefix /// as per convention in the Bitcoin ecosystem. + // TODO: Replace with #[serde(with = "hex")] on Rust 1.47 and remove serde-hex from dependencies #[serde(with = "SerHex::")] pub pubkey: [u8; 33], } diff --git a/comit/src/network/protocols/ethereum_identity.rs b/comit/src/network/protocols/ethereum_identity.rs index 34d74bf7ba..989d366d6b 100644 --- a/comit/src/network/protocols/ethereum_identity.rs +++ b/comit/src/network/protocols/ethereum_identity.rs @@ -1,6 +1,5 @@ -use crate::{identity, network::oneshot_protocol, SharedSwapId}; +use crate::{ethereum, identity, network::oneshot_protocol, SharedSwapId}; use serde::{Deserialize, Serialize}; -use serde_hex::{SerHex, StrictPfx}; use serdebug::SerDebug; /// The message for the Ethereum identity sharing protocol. @@ -9,7 +8,7 @@ pub struct Message { pub swap_id: SharedSwapId, /// An Ethereum address, serialized with a `0x` prefix as per convention in /// the Ethereum ecosystem. - #[serde(with = "SerHex::")] + #[serde(with = "ethereum::serde_hex_data")] pub address: [u8; 20], } diff --git a/comit/src/network/protocols/lightning_identity.rs b/comit/src/network/protocols/lightning_identity.rs index 8515efeee1..5e3edf4cea 100644 --- a/comit/src/network/protocols/lightning_identity.rs +++ b/comit/src/network/protocols/lightning_identity.rs @@ -9,6 +9,7 @@ pub struct Message { pub swap_id: SharedSwapId, /// A Lightning node identifier is a compressed secp256k1 public key, /// serialized without a `0x` prefix. + // TODO: Replace with #[serde(with = "hex")] on Rust 1.47 and remove serde-hex from dependencies #[serde(with = "SerHex::")] pub pubkey: [u8; 33], }