此博客文章的目标受众主要是熟悉区块链和智能合约的开发人员。并非所有开发人员都具有丰富的经济和金融背景。因此,我们建议您阅读关于这些金融方面的博文。
定义“智能发票”
我们的目标是展示我们如何使用智能合约来指定和执行现实世界发票的支付,从而将钱从买方转移到卖方。更具体地说,我们希望实现一个功能,以确保一旦买方接受发票,他就承诺在到截止日期进行付款。
创建以太坊智能合约时会存在某些限制,这些限制会影响如何构建满足这些目标的解决方案。
在以太坊上,不可能执行“触发器”,“事件驱动编程”,“观察者模式”和类似的范例,在这些范例中,某些事情需要作为对其他事情的分离响应发生。因此,我们无法实施在到期日自动执行付款转帐的解决方案。相反,我们创建了一个流程,保证任何人都可以在达到截止日期后触发付款执行。
我们使用三个智能合约来结算真正的贸易发票,它们是:
智能发票
从设计的角度来看,智能发票合同需要尽可能简单。买方承诺支付,因此有必要审计和理解包括此类承诺在内的所有可能后果。
智能发票包含付款金额、截止日期、付款方和付款受益人。受益人可以由当前受益人更改。所有其他字段都是静态的,这对于买方来说非常重要,以便了解他所承诺的内容。
智能发票代币
我们还要将付款标记化。我们通过为智能发票创建一个erc20令牌来实现这一点。这使持有人有权在基础发票结算后获得部分付款。我们这样做是为了说明智能发票的使用案例,例如在结算前出售您的发票代币以获得提前付款。
钱包
买方和卖方都创建并控制他们自己的智能合约钱包。这个钱包可以保持价值,在我们的案例中是DA并I与智能发票发生交互。买方可以承诺通过他的钱包支付给定的智能发票。承诺意味着任何人都可以强制买方钱包在到截止日期支付发票。
端到端测试观察
使用以太坊的最大挑战之一是获得对解决方案的高度信任。对于需要通过实施的大量资金的企业部门尤其如此。
在这个项目中,我们关注的是围绕单元测试的工具和开发。在本节中,我们使用端到端测试来解释创建、标记化和执行发票付款过程中涉及的所有步骤。
用于开发的技术堆栈由:node.js、typescript、solidity和truffle框架组成。以下代码段是端到端测试的一部分。我们还使用一个简单的cli在mainnet上执行了一个引导。在此过程中我们结算了一张真实的发票,并在下面的步骤中为我们的polit添加了Etherscan链接。
1.买方和卖方应各自拥有一个含有以太坊的帐户。
const buyerBalance = await web3.eth.getBalance(buyer);
assert(
new BigNumber(buyerBalance).isGreaterThanOrEqualTo(
web3.utils.toWei(‘10’, ‘ether’),
),
);
const sellerBalance = await web3.eth.getBalance(seller);
assert(
new BigNumber(sellerBalance).isGreaterThanOrEqualTo(
web3.utils.toWei(‘10’, ‘ether’),
),
);
第一步是检查买方和卖方是否在其账户中都有以太币。他们都必须支付在以太坊区块链交易所含的gas费用。
2.买方在其账户中存有DAI(而不是在钱包中)。
const daiDecimals = await mockDAITokenInstance.decimals();
// give buyer 1000 DAI
const daiAmount = new BigNumber(10).pow(daiDecimals).times(1000);
await mockDAITokenInstance.setBalance(buyer, daiAmount.toString(10));
const smartContractBalance = await mockDAITokenInstance.balanceOf
(buyer);
assert.equal(smartContractBalance.toString(10), daiAmount.toString(10));
assert.notExists(buyerWalletInstance);
我们可以使用任何符合ERC20标准的加密货币来完成这个项目,但我们选择了DAI。首先,我们要求使用“稳定币”,因为任何企业都不会接受加密货币汇率风险。其次,我们与Maker建立了合作伙伴关系。
在此步骤中,我们将DAI添加到买方的帐户中。我们使用‘BigNumber’依赖关系来转换所需格式的和(10到18倍1000的幂)。
3.买家创建钱包
assert.notExists(buyerWalletInstance);
buyerWalletInstance = await SmartInvoiceWallet.new(
buyer,
mockDAITokenInstance.address,
{ from: buyer },
);
const buyerWalletAssetTokenAddress = await buyerWalletInstance.
assetToken();
assert.equal(buyerWalletAssetTokenAddress, mockDAITokenInstance.
address);
买方钱包可以持有DAI代币并与智能发票进行交互。
4.卖方创建钱包
assert.notExists(sellerWalletInstance);
sellerWalletInstance = await SmartInvoiceWallet.new(
seller,
mockDAITokenInstance.address,
{ from: seller },
);
const sellerWalletAssetTokenAddress = await sellerWalletInstance.
assetToken();
assert.equal(sellerWalletAssetTokenAddress, mockDAITokenInstance.
address);
5.卖方为买方创建一张贸易发票。
mockInvoice = {
id: ‘xxx-xx–xxxx-xxxx–xxxxxxxx’, // “random” uuid
amount: 70.5,
dueDate: currentTimeStamp() + 60 * 60, // 1h starting from
current time
};
assert.exists(mockInvoice);
通常贸易转移平台上会创建发票。发票ID将用作智能发票标识符(以便买方知道应向谁付款)。为了我们的项目,我们创建了一个对象并添加了所需的属性。
在试点中,我们使用了真正的贸易发票。
6.卖方为贸易转移发票创建智能发票和代币。
assert.exists(sellerWalletInstance);
assert.exists(mockInvoice);
assert.notExists(smartInvoiceTokenInstance);
assert.notExists(smartInvoiceInstance);
const daiDecimals = await mockDAITokenInstance.decimals();
const amount = new BigNumber(10)
.pow(daiDecimals)
.times(mockInvoice.amount);
smartInvoiceTokenInstance = await SmartInvoiceToken.new(
amount,
mockInvoice.dueDate,
mockDAITokenInstance.address,
sellerWalletInstance.address,
buyerWalletInstance.address,
mockInvoice.id,
{ from: seller },
);
const smartInvoiceAddress = await smartInvoiceTokenInstance.
smartInvoice();
// at is mistyped, and does returns a promise
smartInvoiceInstance = await SmartInvoice.at(smartInvoiceAddress);
assert.exists(smartInvoiceInstance);
assert.exists(smartInvoiceAddress);
这是卖方创建智能合同实例的步骤,该实例“wrap”有关自执行发票的所有必要信息。
现在我们创建了一个智能发票。我们只需要买方承诺(在他核实了细节之后)。
7.买方承诺支付智能发票。
const amountToCommit = await smartInvoiceInstance.amount();
const dueDateToCommit = await smartInvoiceInstance.dueDate();
const invoiceIdToCommit = await smartInvoiceInstance.referenceHash();
const daiDecimals = await mockDAITokenInstance.decimals();
const mockInvoiceAmount = new BigNumber(10)
.pow(daiDecimals)
.times(mockInvoice.amount);
// check if commitment value is correct
assert.equal(
amountToCommit.valueOf(),mockInvoiceAmount.valueOf(),
);
assert.equal(dueDateToCommit.toNumber(), mockInvoice.dueDate);
assert.equal(invoiceIdToCommit, mockInvoice.id);
await buyerWalletInstance.commit(
smartInvoiceInstance.address, {from: buyer,}
);
买方验证智能发票中的承诺金额是否与在贸易转移平台上创建的初始发票上确定的金额相同。之后,他承诺在执行之日支付。
8.卖方拥有所有发票代币并确认买方已承诺支付。
const validCommit = await buyerWalletInstance.hasValidCommit(
smartInvoiceInstance.address,{ from: seller },
);
assert.equal(validCommit, true);
const daiDecimals = await mockDAITokenInstance.decimals();
const mockInvoiceAmount = new BigNumber(10)
.pow(daiDecimals)
.times(mockInvoice.amount);
const sellerTokenBalance = await sellerWalletInstance.
invoiceTokenBalance(
smartInvoiceTokenInstance.address, {from: seller}
);
assert.equal(
sellerTokenBalance.valueOf(),mockInvoiceAmount.valueOf(),
);
现在是卖家的行动时间。他首先检查买方是否兑现承诺。至于我们现在关注的是,我们等到截止日期,然后卖方将触发智能发票执行。
9.截止日期到期
const initialBlock = await web3.eth.getBlock(‘latest’);
const timeToAdvance = 60 * 60;
const latestBlock: Block = await advanceTimeAndBlock(timeToAdvance);
assert.notEqual(initialBlock.hash, latestBlock.hash);
// assert if block time increased as expected
assert(
new BigNumber(initialBlock.timestamp)
.plus(timeToAdvance)
.isLessThanOrEqualTo(latestBlock.timestamp)
);
即使在整个这一步骤中没有任何代理实际上采取任何行动,我们认为如何测试时间是否实际按预期进行测试将是非常有趣的。
10.买方将DAI转移到自己的钱包中
const invoiceAmount = await smartInvoiceInstance.amount();
await mockDAITokenInstance.transfer(
buyerWalletInstance.address,
invoiceAmount,
{from: buyer,},
);
const buyerWalletBalance = await buyerWalletInstance.balance();
assert(
new BigNumber(buyerWalletBalance).isGreaterThanOrEqualTo
(invoiceAmount)
);
通常,在到截止日期期,买方应该已经将DAI转移到自己的钱包中。以防买方没有足够的钱支付,在付款的时候,超出了这个项目资金的范围。
11.卖方触发支付智能发票
const canSettle = await buyerWalletInstance
.canSettleSmartInvoice(smartInvoiceInstance.address);
assert.equal(true, canSettle);
// smart invoice is triggered by seller
await buyerWalletInstance.settle(
smartInvoiceInstance.address, {from: seller,}
);
const smartInvoiceTokenInstanceBalance = await mockDAITokenInstance
.balanceOf(smartInvoiceTokenInstance.address,);
const smartInvoiceAmount = await smartInvoiceInstance.amount();
assert.equal(
smartInvoiceTokenInstanceBalance.toString(10),
smartInvoiceAmount.toString(10),
);
是时候卖家结算智能发票了。 我们检查智能发票状态是否设置为“已提交”。这是真的,因为我们看到买方承诺在步骤7付款。此时卖方触发智能发票。
由于每个代币代表正好1 DAI,我们将令牌余额与发票金额进行比较,以查看它们是否匹配。
12.卖方以交换DAI的方式兑换发票代币
const canRedeem = await smartInvoiceTokenInstance.canRedeem();
assert.equal(true, canRedeem);
const sellerWalletBalanceBefore = await sellerWalletInstance.balance
({from: seller});
await sellerWalletInstance
.redeem(smartInvoiceTokenInstance.address, {from: seller,});
const sellerWalletBalanceAfter = await sellerWalletInstance
.balance({ from: seller });
const daiDecimals = await mockDAITokenInstance.decimals();
const daiInvoiceAmount = new BigNumber(10)
.pow(daiDecimals)
.times(mockInvoice.amount);
assert.equal(
daiInvoiceAmount.toString(10),
new BigNumber(sellerWalletBalanceAfter).minus(
sellerWalletBalanceBefore).toString(10)
);
现在卖方已经结算了智能发票,他可以赎回买方欠他的DAI金额。
13.卖方将DAI从钱包转移到自己的账户
const sellerBalanceBefore = await mockDAITokenInstance
.balanceOf(seller, { from: seller });
const daiDecimals = await mockDAITokenInstance.decimals();
const daiInvoiceAmount = new BigNumber(10)
.pow(daiDecimals)
.times(mockInvoice.amount);
await sellerWalletInstance
.transfer(seller, daiInvoiceAmount, {from: seller});
const sellerBalanceAfter = await mockDAITokenInstance
.balanceOf(seller, { from: seller });
assert.equal(
daiInvoiceAmount.toString(10),
new BigNumber(sellerBalanceAfter).minus(sellerBalanceBefore)。
toString(10)
);
我们现在有了一个完整的流程,两个代理在他们之间建立智能发票。如果供应商希望从他的钱包中取出DAI,他可以这样做。我们已经包含了这个测试步骤,这样我们就可以正确地从头到尾地跟踪资金。
最后的想法
这个试点是关于想象智能发票在以太坊世界中的运作方式。 显然,这个项目并不支持大量的发票发送,而是为了说明智能合约和区块链如何适应B2B领域。
评论
查看更多