此博客文章的目標(biāo)受眾主要是熟悉區(qū)塊鏈和智能合約的開發(fā)人員。并非所有開發(fā)人員都具有豐富的經(jīng)濟(jì)和金融背景。因此,我們建議您閱讀關(guān)于這些金融方面的博文。
定義“智能發(fā)票”
我們的目標(biāo)是展示我們?nèi)绾问褂弥悄芎霞s來指定和執(zhí)行現(xiàn)實(shí)世界發(fā)票的支付,從而將錢從買方轉(zhuǎn)移到賣方。更具體地說,我們希望實(shí)現(xiàn)一個功能,以確保一旦買方接受發(fā)票,他就承諾在到截止日期進(jìn)行付款。
創(chuàng)建以太坊智能合約時會存在某些限制,這些限制會影響如何構(gòu)建滿足這些目標(biāo)的解決方案。
在以太坊上,不可能執(zhí)行“觸發(fā)器”,“事件驅(qū)動編程”,“觀察者模式”和類似的范例,在這些范例中,某些事情需要作為對其他事情的分離響應(yīng)發(fā)生。因此,我們無法實(shí)施在到期日自動執(zhí)行付款轉(zhuǎn)帳的解決方案。相反,我們創(chuàng)建了一個流程,保證任何人都可以在達(dá)到截止日期后觸發(fā)付款執(zhí)行。
我們使用三個智能合約來結(jié)算真正的貿(mào)易發(fā)票,它們是:
智能發(fā)票
從設(shè)計(jì)的角度來看,智能發(fā)票合同需要盡可能簡單。買方承諾支付,因此有必要審計(jì)和理解包括此類承諾在內(nèi)的所有可能后果。
智能發(fā)票包含付款金額、截止日期、付款方和付款受益人。受益人可以由當(dāng)前受益人更改。所有其他字段都是靜態(tài)的,這對于買方來說非常重要,以便了解他所承諾的內(nèi)容。
智能發(fā)票代幣
我們還要將付款標(biāo)記化。我們通過為智能發(fā)票創(chuàng)建一個erc20令牌來實(shí)現(xiàn)這一點(diǎn)。這使持有人有權(quán)在基礎(chǔ)發(fā)票結(jié)算后獲得部分付款。我們這樣做是為了說明智能發(fā)票的使用案例,例如在結(jié)算前出售您的發(fā)票代幣以獲得提前付款。
錢包
買方和賣方都創(chuàng)建并控制他們自己的智能合約錢包。這個錢包可以保持價值,在我們的案例中是DA并I與智能發(fā)票發(fā)生交互。買方可以承諾通過他的錢包支付給定的智能發(fā)票。承諾意味著任何人都可以強(qiáng)制買方錢包在到截止日期支付發(fā)票。
端到端測試觀察
使用以太坊的最大挑戰(zhàn)之一是獲得對解決方案的高度信任。對于需要通過實(shí)施的大量資金的企業(yè)部門尤其如此。
在這個項(xiàng)目中,我們關(guān)注的是圍繞單元測試的工具和開發(fā)。在本節(jié)中,我們使用端到端測試來解釋創(chuàng)建、標(biāo)記化和執(zhí)行發(fā)票付款過程中涉及的所有步驟。
用于開發(fā)的技術(shù)堆棧由:node.js、typescript、solidity和truffle框架組成。以下代碼段是端到端測試的一部分。我們還使用一個簡單的cli在mainnet上執(zhí)行了一個引導(dǎo)。在此過程中我們結(jié)算了一張真實(shí)的發(fā)票,并在下面的步驟中為我們的polit添加了Etherscan鏈接。
1.買方和賣方應(yīng)各自擁有一個含有以太坊的帳戶。
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’),
),
);
第一步是檢查買方和賣方是否在其賬戶中都有以太幣。他們都必須支付在以太坊區(qū)塊鏈交易所含的gas費(fèi)用。
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標(biāo)準(zhǔn)的加密貨幣來完成這個項(xiàng)目,但我們選擇了DAI。首先,我們要求使用“穩(wěn)定幣”,因?yàn)槿魏纹髽I(yè)都不會接受加密貨幣匯率風(fēng)險(xiǎn)。其次,我們與Maker建立了合作伙伴關(guān)系。
在此步驟中,我們將DAI添加到買方的帳戶中。我們使用‘BigNumber’依賴關(guān)系來轉(zhuǎn)換所需格式的和(10到18倍1000的冪)。
3.買家創(chuàng)建錢包
assert.notExists(buyerWalletInstance);
buyerWalletInstance = await SmartInvoiceWallet.new(
buyer,
mockDAITokenInstance.address,
{ from: buyer },
);
const buyerWalletAssetTokenAddress = await buyerWalletInstance.
assetToken();
assert.equal(buyerWalletAssetTokenAddress, mockDAITokenInstance.
address);
買方錢包可以持有DAI代幣并與智能發(fā)票進(jìn)行交互。
4.賣方創(chuàng)建錢包
assert.notExists(sellerWalletInstance);
sellerWalletInstance = await SmartInvoiceWallet.new(
seller,
mockDAITokenInstance.address,
{ from: seller },
);
const sellerWalletAssetTokenAddress = await sellerWalletInstance.
assetToken();
assert.equal(sellerWalletAssetTokenAddress, mockDAITokenInstance.
address);
5.賣方為買方創(chuàng)建一張貿(mào)易發(fā)票。
mockInvoice = {
id: ‘xxx-xx–xxxx-xxxx–xxxxxxxx’, // “random” uuid
amount: 70.5,
dueDate: currentTimeStamp() + 60 * 60, // 1h starting from
current time
};
assert.exists(mockInvoice);
通常貿(mào)易轉(zhuǎn)移平臺上會創(chuàng)建發(fā)票。發(fā)票ID將用作智能發(fā)票標(biāo)識符(以便買方知道應(yīng)向誰付款)。為了我們的項(xiàng)目,我們創(chuàng)建了一個對象并添加了所需的屬性。
在試點(diǎn)中,我們使用了真正的貿(mào)易發(fā)票。
6.賣方為貿(mào)易轉(zhuǎn)移發(fā)票創(chuàng)建智能發(fā)票和代幣。
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);
這是賣方創(chuàng)建智能合同實(shí)例的步驟,該實(shí)例“wrap”有關(guān)自執(zhí)行發(fā)票的所有必要信息。
現(xiàn)在我們創(chuàng)建了一個智能發(fā)票。我們只需要買方承諾(在他核實(shí)了細(xì)節(jié)之后)。
7.買方承諾支付智能發(fā)票。
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,}
);
買方驗(yàn)證智能發(fā)票中的承諾金額是否與在貿(mào)易轉(zhuǎn)移平臺上創(chuàng)建的初始發(fā)票上確定的金額相同。之后,他承諾在執(zhí)行之日支付。
8.賣方擁有所有發(fā)票代幣并確認(rèn)買方已承諾支付。
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(),
);
現(xiàn)在是賣家的行動時間。他首先檢查買方是否兌現(xiàn)承諾。至于我們現(xiàn)在關(guān)注的是,我們等到截止日期,然后賣方將觸發(fā)智能發(fā)票執(zhí)行。
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)
);
即使在整個這一步驟中沒有任何代理實(shí)際上采取任何行動,我們認(rèn)為如何測試時間是否實(shí)際按預(yù)期進(jìn)行測試將是非常有趣的。
10.買方將DAI轉(zhuǎn)移到自己的錢包中
const invoiceAmount = await smartInvoiceInstance.amount();
await mockDAITokenInstance.transfer(
buyerWalletInstance.address,
invoiceAmount,
{from: buyer,},
);
const buyerWalletBalance = await buyerWalletInstance.balance();
assert(
new BigNumber(buyerWalletBalance).isGreaterThanOrEqualTo
(invoiceAmount)
);
通常,在到截止日期期,買方應(yīng)該已經(jīng)將DAI轉(zhuǎn)移到自己的錢包中。以防買方?jīng)]有足夠的錢支付,在付款的時候,超出了這個項(xiàng)目資金的范圍。
11.賣方觸發(fā)支付智能發(fā)票
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),
);
是時候賣家結(jié)算智能發(fā)票了。 我們檢查智能發(fā)票狀態(tài)是否設(shè)置為“已提交”。這是真的,因?yàn)槲覀兛吹劫I方承諾在步驟7付款。此時賣方觸發(fā)智能發(fā)票。
由于每個代幣代表正好1 DAI,我們將令牌余額與發(fā)票金額進(jìn)行比較,以查看它們是否匹配。
12.賣方以交換DAI的方式兌換發(fā)票代幣
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)
);
現(xiàn)在賣方已經(jīng)結(jié)算了智能發(fā)票,他可以贖回買方欠他的DAI金額。
13.賣方將DAI從錢包轉(zhuǎn)移到自己的賬戶
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)
);
我們現(xiàn)在有了一個完整的流程,兩個代理在他們之間建立智能發(fā)票。如果供應(yīng)商希望從他的錢包中取出DAI,他可以這樣做。我們已經(jīng)包含了這個測試步驟,這樣我們就可以正確地從頭到尾地跟蹤資金。
最后的想法
這個試點(diǎn)是關(guān)于想象智能發(fā)票在以太坊世界中的運(yùn)作方式。 顯然,這個項(xiàng)目并不支持大量的發(fā)票發(fā)送,而是為了說明智能合約和區(qū)塊鏈如何適應(yīng)B2B領(lǐng)域。
評論
查看更多