在一个经典的商业应用程序中接受加密支付

如何将自定义加密支付方法集成到在线商店

每天 分享 最新 软件 开发 ,Devops,敏捷 ,测试 以及 项目 管理 最新 ,最热门 的 文章 ,每天 花 3分钟 学习 何乐而不为 ,希望 大家 点赞 ,加 关注 ,你的 支持 是我 最大 的 动力 。

电子商务店面在向客户提供加密支付方式方面进展缓慢。加密支付插件或支付网关集成通常不可用,或者它们依赖第三方托管人收集、交换和分发资金。考虑到加密货币持有率和实验比率的不断增长,“用加密货币支付”按钮可以极大地推动销售。

本文演示如何在不依赖第三方服务的情况下将自定义、安全的加密支付方法集成到任何在线商店中。编码和维护智能合同需要相当繁重的工作,我们正在把这项工作移交给块环链建设者常用的工具链Truffle套件。为了在开发期间和应用程序后端提供对区块节点的访问,我们依赖于 Infura 节点,这些节点以慷慨的免费层提供对以太网络的访问。一起使用这些工具将使开发过程更加容易。

场景: Amethon 书店

我们的目标是为可下载的电子书建立一个店面,接受以太区块链的本地货币(“以太”)和 ERC20稳定币(以美元挂钩的支付令牌)作为一种支付方法。从现在开始,我们称之为“ Amethon”。完整的实现可以在附带的 GitHub monorepo 中找到。所有的代码都用Typescript 编写 并且能够被yarn build 或yarn dev 命令编译。

我们将一步一步地指导您完成这个过程,但是熟悉智能契约、以太坊以及 Solidy 编程语言的最低知识可能有助于您一起阅读。我们建议您首先阅读一些基础知识,以熟悉生态系统的基本概念。

Application Structure

存储后端是作为一个 CRUD API 构建的,它本身不连接到任何区块链。它的前端触发对该 API 的支付请求,客户使用他们的加密钱包完成。

Amethon 被设计成一个“传统的”电子商务应用程序,它负责业务逻辑,除了支付本身之外不依赖任何上链数据。在结帐过程中,后端发出 PaymentRequest 对象,这些对象携带一个唯一标识符(例如“发票号码”) ,用户将其附加到支付交易中。

后台守护进程侦听各自的契约事件,并在检测到付款时更新商店的数据库。

Amethon 的支付结算

收款人合约

在 Amethon 中心,PaymentReceiversmart 合同代表店面所有者接受和托管付款。

每次用户向 PaymentReceiver 合同发送资金时,都会发送一个 PaymentReceivedevent,其中包含有关支付的来源(客户的 Etherum 帐户)、其总价值、所使用的 ERC20令牌合同地址以及指向后端数据库条目的 paymentID 的信息。

TypeScript-JSX

event PaymentReceived( address indexed buyer, uint256 value, address token, bytes32 paymentId );

以太合同的作用类似于基于用户(即“外部拥有”/EOA)的帐户,并在部署时获得自己的帐户地址。接收本地以太货币需要实现接收和回退功能,这些功能在某人将以太资金转移到合同时被调用,并且没有其他功能签名与调用匹配:

TypeScript-JSX

receive() external payable { emit PaymentReceived(msg.sender, msg.value, ETH_ADDRESS, bytes32(0)); } fallback() external payable { emit PaymentReceived( msg.sender, msg.value, ETH_ADDRESS, bytes32(msg.data)); }

官方的 Soliity 文档指出了这些函数之间的细微差别: 当传入的事务不包含额外数据时,就会调用 Receiveis,否则就会调用回退。以太本身的本地货币不是 ERC20令牌,除了作为计数单位外没有其他用途。然而,它有一个可识别的地址(0xEeeeeEeeEeeEeeEeeEeeEeeEeeEeeeeeeeeeeEEEE) ,我们使用信号以太支付在我们的 PaymentReceivedevents。

然而,以太传输有一个主要的缺点: 接收时允许的计算量非常低。客户送来的天然气只能让我们发出一个事件,但不能将资金重定向到店主的原始地址。因此,接收者契约保留所有传入的以太,并允许商店所有者在任何时候将它们释放到自己的帐户中:

TypeScript-JSX

function getBalance() public view returns (uint256) { return address(this).balance;}function release() external onlyOwner { (bool ok, ) = _owner.call{value: getBalance()}(“”); require(ok, “Failed to release Eth”);}

由于历史原因,接受 ERC20令牌作为支付稍微有点困难。在2015年,最初规范的作者无法预测即将到来的需求,因此使 ERC20标准的接口尽可能简单。最值得注意的是,ERC20合同不能保证通知收件人有关转移的信息,所以当 ERC20令牌被转移到我们的 PaymentReceiver 时,它无法执行代码。

ERC20生态系统已经发生了演变,现在包括额外的规格。例如,EIP1363标准解决了这个问题。不幸的是,你不能依靠主要的稳定币平台来实现它。

因此,Amethon 必须以“经典”的方式接受 ERC20象征性付款。契约不会在不知情的情况下“丢弃”令牌,而是代表客户负责传输。这就要求用户首先允许合同处理一定数额的资金。这不方便地要求用户在与实际支付方法交互之前首先将 Approval 事务传输到 ERC20令牌合同。EIP-2612可能会改善这种情况,但是,我们必须按照旧的规则玩暂时。

TypeScript-JSX

function payWithErc20( IERC20 erc20, uint256 amount, uint256 paymentId ) external { erc20.transferFrom(msg.sender, _owner, amount); emit PaymentReceived( msg.sender, amount, address(erc20), bytes32(paymentId)

编译、部署和可变安全性

有几个工具链允许开发人员编译、部署和与 Etherum 智能契约进行交互,但最先进的工具链之一是 Truffle Suite。它带有一个基于 Ganache 的内置开发块链和一个迁移概念,允许您自动化并安全地运行契约部署。

在“真正的”区块链基础设施上部署合同,比如以太网测试网,需要两件事: 一个连接到区块链节点的以太网提供商和一个账户的私钥/钱包助记符或者一个可以代表一个账户签署交易的钱包连接。该帐户还需要有一些(测试网)以太在它支付天然气费用期间部署。

MetaMask 就是干这个的。创建一个新的帐户,你不会使用其他任何东西,但部署(它将成为“所有者”的合同) ,并与一些以太使用你首选的测试网的水龙头(我们推荐 Paradigm)。通常情况下,你现在可以导出账户的私钥(“ Account Details”> “ Export Private Key”)并将其与你的开发环境连接起来,但是为了规避这个工作流程所隐含的所有安全问题,Truffle 提供了一个专用的仪表板网络和网络应用程序,可以用来在浏览器中使用 Metamask 签署合同部署之类的事务。要启动它,在一个新的终端窗口中执行 truffle dashboard,并使用一个带有活动 Metamask 扩展的浏览器访问 http://localhost:24012/。

使用 truffle 的仪表板在不暴露私钥的情况下对事务进行签名

Amethon 项目还依赖于各种秘密设置。注意,由于 dotenv-flow 的工作方式,。Envfiles 包含示例或公开可见的设置,这些设置被 gitignored.env.localfiles 覆盖。收到。并覆盖它们的值。

若要将本地环境连接到以太网络,请访问同步块链节点。当然,你可以下载其中一个客户端,然后等待它在你的机器上同步,但是将你的应用程序连接到 Ethereum 的节点上要方便得多,这些节点是以服务的形式提供的,其中最著名的是 Infura。他们的免费层为您提供三种不同的访问密钥和每月10万的 RPC 请求,支持范围广泛的以太网络。

注册之后,注意你的 INFURA 密钥并把它放在你的合同中. env.localas INFURA _ KEY。

如果你想与合同互动,例如在 Kovan 网络上,只需在所有的 truffle 命令中添加相应的 truffle 配置和 -network Kovan 选项。您甚至可以启动一个交互式控制台: 纱松露控制台——网络 Kovan。在本地测试契约不需要任何特殊的设置过程。为了让我们的生活变得简单,我们使用 Metamask 通过truffle仪表板提供者注入的提供者和签名者。

改变为contracts 文件夹 并运行yarn truffle develop.这将启动一个本地区块链与预先资助的帐户,并打开一个连接的控制台上。要将你的 Metamask 钱包连接到开发网络,使用 http://localhost:9545作为其 RPC 端点创建一个新的网络。当链条开始时,请注意列出的帐户: 您可以导入他们的私人密钥到您的 Metamask 钱包发送交易代表您的本地区块链。

输入compile来一次编译所有契约,并使用migrate将它们部署到本地链中。 你可以通过请求它们当前部署的实例来与契约交互,并像这样调用它的函数:

TypeScript-JSX

pr = await PaymentReceiver.deployed()balance = await pr.getBalance()

一旦您对结果感到满意,您就可以将它们部署到公共测试网(或 mainnet)上,以及:

Shell

yarn truffle migrate –interactive –network dashboard

The Backend

存储 API/CRUD

我们的后端提供了一个 JSON API 来与高层次的支付实体进行交互。我们决定使用 TypeORM 和本地 SQLite 数据库来支持 Books 和 PaymentRequest 的实体。书籍代表我们商店的主要实体,有一个零售价格,以美分表示。为了最初在数据库中添加书籍,可以使用 companyingSeed.ts 文件。编译完文件后,可以通过 invokingnode build/Seed.js 执行它。

TypeScript

//backend/src/entities/Book.tsimport { Entity, Column, PrimaryColumn, OneToMany } from “typeorm”;import { PaymentRequest } from “./PaymentRequest”;@Entity()export class Book { @PrimaryColumn() ISBN: string; @Column() title: string;

注意: 在任何计算机系统上都强烈建议不要将货币值存储为浮动值,因为对浮动值进行操作肯定会引入精度错误。这也是为什么所有的加密令牌都使用18位十进制数,而 Soliity 甚至没有 float 数据类型的原因。1以太实际上代表“10000000000000000000”为最小的以太单位。

对于打算从 Amethon 购买书籍的用户,首先通过调用/books/: isbn/orderpath 为他们的商品创建一个个人支付请求。这将创建一个新的唯一标识符,必须随每个请求一起发送。

我们在这里使用的是普通整数,但是,对于实际的用例,您将使用更复杂的东西。唯一的限制是 id 的二进制长度必须适合32字节(uint256)。EachPaymentRequest 继承书籍的零售价值(以美分为单位) ,并记录客户的地址,在购买过程中确定 Hash 和 paidUSDCentwill。

TypeScript

//backend/src/entities/PaymentRequest.ts@Entity()export class PaymentRequest { @PrimaryGeneratedColumn() id: number; @Column(“varchar”, { nullable: true }) fulfilledHash: string | null; @Column() address: string; @Column() priceInUSDCent: number; @Column(“mediumint”, { nullable: true }) paidUSDCent: number; @ManyToOne(() => Book, (book) => book.payments) book: Book;}

创建 PaymentRequesttity 的初始订单请求如下:

TypeScript

POST http://localhost:3001/books/978-0060850524/orderContent-Type: application/json{ “address”: “0xceeca1AFA5FfF2Fe43ebE1F5b82ca9Deb6DE3E42”}—>{ “paymentRequest”: { “book”: { “ISBN”: “978-0060850524”, “title”: “Brave New World”, “retailUSDCent”: 1034 }, “address”: “0xceeca1AFA5FfF2Fe43ebE1F5b82ca9Deb6DE3E42”, “priceInUSDCent”: 1034, “fulfilledHash”: null, “paidUSDCent”: null, “id”: 6 }, “receiver”: “0x7A08b6002bec4B52907B4Ac26f321Dfe279B63E9”}

区块链监听器后台服务

查询区块链的状态树不会花费客户机任何气体,但节点仍然需要计算。当这些操作变得计算量太大时,它们可能会超时。对于实时交互,强烈建议不要轮询链状态,而是监听事务发出的事件。这需要使用支持 WebSocket 的提供程序,因此一定要使用以 wss://as URL 方案开头的 Infura 端点,用于后端的 PROVIDER _ RPC 环境变量。然后,您可以启动后端的 daemon.tsscript 并在任何链上侦听 PaymentReceivedevents:

TypeScript

//backend/src/daemon.ts const web3 = new Web3(process.env.PROVIDER_RPC as string); const paymentReceiver = new web3.eth.Contract( paymentReceiverAbi as AbiItem[], process.env.PAYMENT_RECEIVER_CONTRACT as string ); const emitter = paymentReceiver.events.PaymentReceived({ fromBlock: “0”, });

注意我们是如何用一个应用二进制接口实例化契约实例的。Soliity 编译器生成 ABI,并为 RPC 客户机提供有关如何编码事务以调用和解码智能契约上的函数、事件或参数的信息。

一旦实例化,您就可以在契约的 PaymentRecected 日志(从块0开始)上钩住一个侦听器,并在收到后处理它们。

因为 Amethon 支持 Ether 和 stablecoin (“ US”)支付,所以守护进程的 handlePaymentEventmethod 首先检查用户支付中使用了哪个令牌,并在需要时计算其美元价值:

TypeScript

//backend/src/daemon.tsconst ETH_USD_CENT = 2_200 * 100;const ACCEPTED_USD_TOKENS = (process.env.STABLECOINS as string).split(“,”);const NATIVE_ETH = “0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE”;const handlePaymentEvent = async (event: PaymentReceivedEvent) => { const args = event.returnValues; const paymentId = web3.utils.hexToNumber(args.paymentId); const decimalValue = web3.utils.fromWei(args.value); const payment = await paymentRepo.findOne({ where: { id: paymentId } }); let valInUSDCents; if (args.token === NATIVE_ETH) { valInUSDCents = parseFloat(decimalValue) * ETH_USD_CENT; } else { if (!ACCEPTED_USD_TOKENS.includes(args.token)) { return console.error(“payments of that token are not supported”); } valInUSDCents = parseFloat(decimalValue) * 100; } if (valInUSDCents < payment.priceInUSDCent) { return console.error(`payment [${paymentId}] not sufficient`); } payment.paidUSDCent = valInUSDCents; payment.fulfilledHash = event.transactionHash; await paymentRepo.save(payment);};

The Frontend

我们书店的前端是建立在官方的创建反应应用程序模板与类型支持,并使用 Tailwind 的基本样式。它支持所有已知的 CRA 脚本,因此您可以在创建自己的脚本之后通过纱线启动本地脚本。本地文件,包含之前创建的支付接收方和 stablecoin 合同地址。

注意: CRA5将他们的 webpack 依赖关系转移到了一个不再支持浏览器中节点填充的版本。这打破了今天几乎所有与以太坊相关的项目的构建。避免弹出的一个常见解决方案是挂钩到 CRA 构建过程中。我们正在使用 response-app-rewire,但是你可以简单地呆在 CRA4,直到社区提出一个更好的解决方案。

连接 WEB3钱包

任何 Dapp 的关键部分都是连接到用户的钱包。您可以尝试按照正式的 MetaMask 文档手动连接该进程,但我们强烈建议使用适当的 React 库。我们发现 Noah Zinsmeister 的 web3反应是最好的。检测和连接 web3客户端归结为以下代码(ConnectButton.tsx) :

TypeScript

//frontend/src/components/ConnectButton.tsimport { useWeb3React } from “@web3-react/core”;import { InjectedConnector } from “@web3-react/injected-connector”;import React from “react”;import Web3 from “web3”;export const injectedConnector = new InjectedConnector({ supportedChainIds: [42, 1337, 31337], //Kovan, Truffle, Hardhat});export const ConnectButton = () => { const { activate, account, active } = useWeb3React(); const connect = () => { activate(injectedConnector, console.error); }; return active ? ( connected as: {account} ) : ( Connect );};

通过将应用程序的代码封装在 上下文中,您可以从任何组件使用 useWeb3Reacthook 访问 web3提供程序、帐户和连接状态。由于 Web3React 与所使用的 web3库(Web3.js 或 ethers.js)是不可知的,因此您必须提供一个回调,以生成一个连接的“库”:

TypeScript-JSX

//frontend/src/App.tsximport Web3 from “web3”;function getWeb3Library(provider: any) { return new Web3(provider);}

支付流程

从 Amethon 后端加载可用的图书后, 组件首先检查该用户的付款是否已经处理,然后显示打包在 组件中的所有支持的付款选项。

Paying With ETH

负责启动对 PaymentReceiveragreement 的直接以太传输。由于这些调用并不直接与契约接口交互,我们甚至不需要初始化契约实例:

TypeScript-JSX

//frontend/src/components/PayButton.tsxconst weiPrice = usdInEth(paymentRequest.priceInUSDCent);const tx = web3.eth.sendTransaction({ from: account, //the current user to: paymentRequest.receiver.options.address, //the PaymentReceiver contract address value: weiPrice, //the eth price in wei (10**18) data: paymentRequest.idUint256, //the paymentRequest’s id, converted to a uint256 hex string});const receipt = await tx;onConfirmed(receipt);

如前所述,由于新的事务带有 msg.data 字段,Soliity 的约定触发 PaymentReceiver 的回退()外部支付函数,该函数发出一个带有 Ether 令牌地址的 PaymentReceivedevent。这由被守护的链监听器接收,该监听器相应地更新后端的数据库状态。

静态助手函数负责将当前的美元价格转换为以太值。在一个真实的场景中,从值得信赖的第三方(如 Coingecko)或像 Uniswap 这样的 DEX 查询汇率。这样做允许您扩展 Amethon 以接受任意令牌作为支付。

TypeScript

//frontend/src/modules/index.tsconst ETH_USD_CENT = 2_200 * 100;export const usdInEth = (usdCent: number) => { const eth = (usdCent / ETH_USD_CENT).toString(); const wei = Web3.utils.toWei(eth, “ether”); return wei;};

用 ERC20稳定币支付

由于前面提到的原因,从用户的角度来看,ERC20令牌中的支付稍微复杂一些,因为不能简单地删除契约中的令牌。像几乎所有具有类似用例的人一样,我们必须首先请求用户允许我们的 PaymentReceiver 合同转移他们的资金,并调用代表用户转移请求资金的实际 payWithEerc20方法。

下面是 PayWithStableButton 对选定的 ERC20令牌授予权限的代码:

TypeScript

//frontend/src/components/PayWithStableButton.tsxconst contract = new web3.eth.Contract( IERC20ABI as AbiItem[], process.env.REACT_APP_STABLECOINS);const appr = await coin.methods .approve( paymentRequest.receiver.options.address, //receiver contract’s address price // USD value in wei precision (1$ = 10^18wei) ) .send({ from: account, });

请注意,设置 ERC20令牌的契约实例所需的 ABI 接收一般的 IERC20 ABI。我们使用从 OpenZeppelin 的官方库中生成的 ABI,但是任何其他生成的 ABI 都可以完成这项工作。在批准转让后,我们可以开始付款:

TypeScript-JSX

//frontend/src/components/PayWithStableButton.tsxconst contract = new web3.eth.Contract( PaymentReceiverAbi as AbiItem[], paymentRequest.receiver.options.address);const tx = await contract.methods .payWithErc20( process.env.REACT_APP_STABLECOINS, //identifies the ERC20 contract weiPrice, //price in USD (it’s a stablecoin) paymentRequest.idUint256 //the paymentRequest’s id as uint256 )

签署下载请求

最后,我们的客户可以下载他们的电子书。但是有一个问题: 既然我们没有“登录”用户,我们如何确保只有真正为内容付费的用户才能调用我们的下载路径?答案是加密签名。在将用户重定向到我们的后端之前, 组件允许用户签署一个独特的消息,该消息被提交作为帐户控制的证明:

TypeScript-JSX

//frontend/src/components/DownloadButton.tsxconst download = async () => { const url = `${process.env.REACT_APP_BOOK_SERVER}/books/${book.ISBN}/download`; const nonce = Web3.utils.randomHex(32); const dataToSign = Web3.utils.keccak256(`${account}${book.ISBN}${nonce}`); const signature = await web3.eth.personal.sign(dataToSign, account, “”); const resp = await ( await axios.post(

后端的下载路由可以恢复签名者的地址,方法是按照与用户以前相同的方式组合消息,并使用消息和提供的签名调用加密套件的 ecRecovery 方法。如果恢复的地址与我们数据库中已完成的 PaymentRequest 匹配,我们知道我们可以允许访问请求的电子书资源:

TypeScript

//backend/src/server.tsapp.post( “/books/:isbn/download”, async (req: DownloadBookRequest, res: Response) => { const { signature, address, nonce } = req.body; //rebuild the message the user created on their frontend const signedMessage = Web3.utils.keccak256( `${address}${req.params.isbn}${nonce}` );

这里提供的帐户所有权证明仍然不是完全正确的。任何知道所购物品的有效签名的人都可以成功地调用下载路由。最后的修复方法是首先在后端上创建随机消息,然后让客户签名并批准它。由于用户无法理解他们应该签署的混乱的十六进制代码,他们不会知道我们是否会欺骗他们签署另一个有效的交易,这可能会危及他们的帐户。

虽然我们已经通过使用 web3的 eth.Personal.signmethod 避免了这种攻击向量,但是最好以人性化的方式显示要签名的消息。这就是 EIP-712达到的目标ーー MetaMask 已经支持这一标准。

结论及下一步

对于开发者来说,接受电子商务网站的付款从来都不是一件容易的事情。尽管 web3生态系统允许店面接受数字货币,但独立于服务的插件解决方案的可用性不足。本文演示了一种安全、简单和自定义的方法来请求和接收加密支付。

还有进一步发展的空间。以太网上 ERC20传输的天然气成本远远超过我们的账面价格。低价物品的加密支付在天然气友好的环境中是有意义的,比如 Gnosis Chain (他们的“本地”以太货币是 DAI,所以你甚至不用担心稳定币的转移)或者 Arbitrum。您还可以使用购物车签出来扩展后端,或者使用 DEXes 将任何传入的 ERC20令牌交换到您首选的货币。

毕竟,web3的承诺是允许没有中间商的直接货币交易,并为希望吸引懂加密技术的客户的在线商店增加重大价值。

郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
(0)
用户投稿
上一篇 2022年7月10日
下一篇 2022年7月10日

相关推荐

联系我们

联系邮箱:admin#wlmqw.com
工作时间:周一至周五,10:30-18:30,节假日休息