Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pvolnov committed Sep 24, 2023
0 parents commit 6468687
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 1,6 @@
/node_modules
yarn.lock
chains.txt
deb.js
main.js
main2.js
5 changes: 5 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/lit-demo-simple-string-encrypt-nodejs.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 1,56 @@
# DAI <> USD P2P swap protocol


**Problem:** P2P crypto swaps require bank transfer confirmation. This leads to centralization, delays and errors.

*example: swap DAI to USD on Bank of America*

**Solution:** Protocol for P2P swaps based on onchain wire transfer confirmation

## How it works
1. Bob creates an deal to sell DAI
2. Alice accepts the deal and froze Bob's DAI
3. Alice sends USD via Zelle
4. Alice creates onchain proof of her Zelle transfer
5. Alice using this confirmation closes the deal and takes Bob's DAI


## Onchain proof for Bank (Zelle) transfer

The confirmation works with Plaid and the LIT Protocol nodes

1. Alice connects Plaid and creates an API key with access to transaction history from the bank
2. Alice calls Programmable PKPs action on LIT Protocol with Plaid transactionId and API key ([demo.js](lit-action/demo.js))
3. Inside the `LitAction` executes the request on Plaid and verifies that the transaction actually exists in Alice's bank account. ([litActionPlaidCheck.js](lit-action/litActionPlaidCheck.js))
4. The `LitAction` signs the transaction information using the decentralized `signEcdsa()`
5. Adisa adds signed transaction to the "vault" smart contract ([plaid-transactions-storage.sol](contracts/plaid-transactions-storage.sol)) and now everyone can verify that she actually made the transaction from her bank account.

![Cover.png](res/Cover.png)

## Step-by-step-swap
1. Bob creates an deal to sell DAI on [p2p-market](contracts/p2p-market.sol) (`createPaymentRequest(...)`)
2. Alice accepts the deal and froze Bob's DAI (`confirmPayment(...)`)
3. Alice sends USD via Zelle
4. Alice creates onchain proof of her Zelle transfer on [plaid-transactions-storage](contracts/plaid-transactions-storage.sol) (`addTransaction(...)`)
5. Alice using this confirmation closes the deal and takes Bob's DAI on [p2p-market](contracts/p2p-market.sol) (`reservePayment(...)`)


## Main components:

- [p2p-market.sol](contracts/p2p-market.sol) - P2P Market smart contract
- [plaid-transactions-storage.sol](contracts/plaid-transactions-storage.sol) - onchain wire transactions storage
- [litActionPlaidCheck.js](lit-action/litActionPlaidCheck.js) - LIT Action to verify Plaid requests
- [bos](bos) - Frontend

# Demo:



Url: https://bos.gg/#/azbang.near/widget/peer2peer

# Source
- BOS Component: https://bos.gg/#/azbang.near/widget/peer2peer
- LIT-Protocol action ipfsId: `QmVcetXaAnDcHmpX6cWxsGKGSoid4buVK2PkXQEDmgh6wQ`
- LIT-Protocol PKPs: https://explorer.litprotocol.com/pkps/110216617645918104171481682055149393821345448971586480839222858944196209976493
- Goerli Plaid transactions endpoint: https://goerli.etherscan.io/address/0x00e42dc2248f37a28d39f790d49230e2ddd37b99
- Goerli P2P Market: https://goerli.etherscan.io/address/0xa3724030aA74016d1AE1e1B54bD79608F0E5866F
133 changes: 133 additions & 0 deletions contracts/p2p-market.sol
Original file line number Diff line number Diff line change
@@ -0,0 1,133 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}

interface ITransactionChecker {
struct Transaction {
int256 amount;
string name;
int256 timestamp;
address sender;
}

function getTransaction(bytes32 transactionId) external view returns (Transaction memory);
}

contract P2PPayments {
struct PaymentRequest {
address payable requester;
uint256 amount;
bytes32 zelleEmailHash;
address payable executor;
}

IERC20 public usdtToken;
ITransactionChecker public checkerContract;

mapping(uint256 => PaymentRequest) public requests;
uint256 public nextRequestId = 1;
uint256 public minEthReserveAmount = 0.001 ether;
address owner;

// Events to log payment-related actions
event PaymentRequested(uint256 requestId, address indexed requester, uint256 amount, bytes32 zelleEmailHash);
event PaymentReserved(uint256 requestId, address indexed reserver);
event PaymentConfirmed(uint256 requestId, address indexed confirmer);

// Constructor to initialize the contract with addresses of ERC20 token and TransactionChecker
constructor(address _usdtToken, address _checkerContract) {
usdtToken = IERC20(_usdtToken);
checkerContract = ITransactionChecker(_checkerContract);
owner = msg.sender;
}

// Modifier to allow only the requester to perform certain actions
modifier onlyRequester(uint256 requestId) {
require(msg.sender == requests[requestId].requester, "Only the requester can perform this action");
_;
}

// Modifier to ensure that a payment has not been reserved
modifier paymentNotReserved(uint256 requestId) {
require(requests[requestId].executor == address(0), "Payment already reserved");
_;
}

// Function to create a new payment request
function createPaymentRequest(uint256 amount, bytes32 _zelleEmailHash) external {
require(amount == 10 || amount == 100, "Invalid amount");

requests[nextRequestId] = PaymentRequest({
requester: payable(msg.sender),
amount: amount,
zelleEmailHash: _zelleEmailHash,
executor: payable(address(0))
});

emit PaymentRequested(nextRequestId, msg.sender, amount, _zelleEmailHash);
nextRequestId ;
}

// reserve a payment so avoid parallel execution
function reservePayment(uint256 requestId) external payable paymentNotReserved(requestId) {
require(msg.value >= minEthReserveAmount, "Transaction must include at least 0.001 ETH");

PaymentRequest storage request = requests[requestId];

usdtToken.transferFrom(request.requester, address(this), request.amount);
request.executor = payable(msg.sender);

emit PaymentReserved(requestId, msg.sender);
}

// Modifier to allow only the owner of the contract to call certain functions
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can call this function");
_;
}

// set the minimum ETH fee amount
function set_minEthReserveAmount(uint256 _minEthReserveAmount) public onlyOwner {
minEthReserveAmount = _minEthReserveAmount;
}

// withdraw ETH from the contract (only owner)
function withdrawEther() public onlyOwner {
payable(owner).transfer(address(this).balance);
}

// confirm a payment
function confirmPayment(uint256 requestId, bytes32 transactionId) external onlyRequester(requestId) {
PaymentRequest storage request = requests[requestId];

require(request.executor == msg.sender, "Only executor can close request");

// check proof from LIT protocol gateaway
ITransactionChecker.Transaction memory transaction = checkerContract.getTransaction(transactionId);
require(keccak256(abi.encodePacked(transaction.name)) == request.zelleEmailHash, "Invalid transaction name hash");

// complete transfer
usdtToken.transfer(request.executor, request.amount);

delete requests[requestId];
emit PaymentConfirmed(requestId, msg.sender);
}

// Function to get payment requests by their IDs
function getPaymentRequestsByIds(uint256[] memory requestIds) external view returns (PaymentRequest[] memory) {
uint256 count = requestIds.length;
PaymentRequest[] memory result = new PaymentRequest[](count);

for (uint256 i = 0; i < count; i ) {
uint256 requestId = requestIds[i];
require(requestId > 0 && requestId < nextRequestId, "Invalid request ID");
result[i] = requests[requestId];
}

return result;
}
}
44 changes: 44 additions & 0 deletions contracts/plaid-transactions-storage.sol
Original file line number Diff line number Diff line change
@@ -0,0 1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TransactionStorage {
struct Transaction {
int256 amount;
string name;
int256 timestamp;
address sender;
}

event NewTransaction(bytes32 transaction_id, int256 amount);
address constant SIGNER_ADDRESS = 0xe156276E5d9c5920661F90C3c35906adefD6C437;
mapping(bytes32 => Transaction) private transactions;

// Function to add a new transaction to the storage
function addTransaction(bytes memory serializedData, bytes32 r, bytes32 s, uint8 v) public {
// Calculate the hash of the serialized data
bytes32 hash = keccak256(serializedData);

// Recover the address of the signer using the provided signature
address signer = ecrecover(hash, v, r, s);

// Ensure that the signer is the expected signer from LIT Protocol
require(signer == SIGNER_ADDRESS, "Invalid signature");

// Decode the serialized data to extract transaction details
(bytes32 transaction_id, int256 amount, string memory name, int256 timestamp) = abi.decode(serializedData, (bytes32, int256, string, int256));

Transaction memory trx = Transaction({
amount: amount,
name: name,
timestamp: timestamp,
sender: msg.sender
});

emit NewTransaction(transaction_id, amount);
transactions[transaction_id] = trx;
}

function getTransaction(bytes32 transaction_id) public view returns (Transaction memory) {
return transactions[transaction_id];
}
}
44 changes: 44 additions & 0 deletions lit-action/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 1,44 @@
// Import the LitJsSdk library from the '@lit-protocol/lit-node-client-nodejs' package
import LitJsSdk from '@lit-protocol/lit-node-client-nodejs';

// Define constants
const LIT_NETWORK = "serrano";
const PKP_PUBLIC_KEY = "0x04f80a948f038f5d69855268f749457d5b465b78fd7bf603de13bd4bf01d718175bf512c828414e227a8289e7512b331658394c4d37a34aec3eca9c585056b7180";
const IPFS_ID = "QmTioWBHeq1rSKdtBZwwsmw59WabAmPr6c8dVcWDHiP7cY";
const AUTH_SIGNATURE = authSig_acc2; // Make sure 'authSig_acc2' is defined earlier
const CLIENT_PARAMS = {
chain: "ethereum",
publicKey: PKP_PUBLIC_KEY,
sigName: "sig1",
"access_token": "access-sandbox-ba9ee489-90fd-4b20-be28-96f9828cc5da",
"start_date": "2023-09-23",
"end_date": "2023-09-23",
"tr_num": 0,
"client_id": "650ec5e216ecbb001b12ca1d",
"secret": "3618a4c3bb886629ad11e32c2e139b"
};

const encryptDecryptString = async () => {
// Create a new LitNodeClientNodeJs instance
const litNodeClient = new LitJsSdk.LitNodeClientNodeJs({
alertWhenUnauthorized: false,
litNetwork: LIT_NETWORK,
debug: true,
});

// Connect to the Lit Node
await litNodeClient.connect();

// Execute a JavaScript function on the Lit Node using constants
const { signatures, response, logs } = await litNodeClient.executeJs({
ipfsId: IPFS_ID,
authSig: AUTH_SIGNATURE,
jsParams: CLIENT_PARAMS,
});

console.log(response);
console.log(signatures);
console.log(logs);
}

encryptDecryptString();
57 changes: 57 additions & 0 deletions lit-action/litActionPlaidCheck.js
Original file line number Diff line number Diff line change
@@ -0,0 1,57 @@
const emailRegex = /\S @\S \.\S /;

const checkAndSignResponse = async () => {
const url = "https://sandbox.plaid.com/transactions/get";
const bdata = {
access_token, start_date, end_date
};
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"PLAID-CLIENT-ID": client_id,
"PLAID-SECRET": secret,
},
body: JSON.stringify(bdata)
}).then((response) => response.json());


const firstEmailMatch = resp.transactions[tr_num].name.match(emailRegex);
if (firstEmailMatch) {
name = firstEmailMatch[0]
}
else {
name = resp.transactions[tr_num].name
}

const trx = {
transaction_id: resp.transactions[tr_num].transaction_id,
amount: resp.transactions[tr_num].amount,
name: name,
timestamp: Date.parse(resp.transactions[tr_num].datetime) || Date.parse(resp.transactions[tr_num].date)
};

const transactionHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(trx.transaction_id));

const encodedTrx = ethers.utils.defaultAbiCoder.encode(
['bytes32', 'int256', 'string', 'int256'],
[
transactionHash,
ethers.utils.parseUnits(trx.amount.toString(), 2),
trx.name,
trx.timestamp,
]
);

const toSign = ethers.utils.keccak256(encodedTrx);
await LitActions.signEcdsa({ toSign, publicKey, sigName });

const result = {
data: encodedTrx,
transactionId: transactionHash,
}

LitActions.setResponse({ response: JSON.stringify(result) });
};

checkAndSignResponse();
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 1,14 @@
{
"type": "module",
"scripts": {
"dev": "node main.js"
},
"dependencies": {
"@lit-protocol/contracts-sdk": "^2.2.54",
"@lit-protocol/lit-node-client": "^2.2.24",
"@lit-protocol/pkp-ethers": "^2.2.54",
"ethers": "^6.7.1",
"siwe": "^1.1.6",
"uint8arrays": "^3.0.0"
}
}
Empty file added res/Cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 6468687

Please sign in to comment.