Skip to content

Commit

Permalink
[Relay] Implement the ability to switch to payable points
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelKim20 committed Oct 4, 2023
1 parent ff0c647 commit bcee3b9
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 7 deletions.
69 changes: 69 additions & 0 deletions packages/relay/src/routers/DefaultRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,21 @@ export class DefaultRouter {
],
this.setPointType.bind(this)
);

// 사용가능한 포인트로 전환
this.app.post(
"/changeToPayablePoint",
[
body("phone")
.exists()
.matches(/^(0x)[0-9a-f]{64}$/i),
body("account").exists().isEthereumAddress(),
body("signature")
.exists()
.matches(/^(0x)[0-9a-f]{130}$/i),
],
this.changeToPayablePoint.bind(this)
);
}

private async getHealthStatus(req: express.Request, res: express.Response) {
Expand Down Expand Up @@ -342,6 +357,7 @@ export class DefaultRouter {
this.releaseRelaySigner(signerItem);
}
}

/**
* 포인트의 종류를 선택한다.
* POST /setPointType
Expand Down Expand Up @@ -394,4 +410,57 @@ export class DefaultRouter {
this.releaseRelaySigner(signerItem);
}
}

/**
* 포인트의 종류를 선택한다.
* POST /changeToPayablePoint
* @private
*/
private async changeToPayablePoint(req: express.Request, res: express.Response) {
logger.http(`POST /changeToPayablePoint`);

const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(200).json(
this.makeResponseData(501, undefined, {
message: "Failed to check the validity of parameters.",
validation: errors.array(),
})
);
}

const signerItem = await this.getRelaySigner();
try {
const phone: string = String(req.body.phone);
const account: string = String(req.body.account); // 구매자의 주소
const signature: string = String(req.body.signature); // 서명

// 서명검증
const userNonce = await (await this.getLedgerContract()).nonceOf(account);
if (!ContractUtils.verifyChangePayablePoint(phone, account, userNonce, signature))
return res.status(200).json(
this.makeResponseData(500, undefined, {
message: "Signature is not valid.",
})
);

const tx = await (await this.getLedgerContract())
.connect(signerItem.signer)
.changeToPayablePoint(phone, account, signature);

logger.http(`TxHash(setPointType): `, tx.hash);
return res.status(200).json(this.makeResponseData(200, { txHash: tx.hash }));
} catch (error: any) {
let message = ContractUtils.cacheEVMError(error as any);
if (message === "") message = "Failed change point type";
logger.error(`POST /setPointType :`, message);
return res.status(200).json(
this.makeResponseData(500, undefined, {
message,
})
);
} finally {
this.releaseRelaySigner(signerItem);
}
}
}
56 changes: 50 additions & 6 deletions packages/relay/src/utils/ContractUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as crypto from "crypto";
import { BigNumberish, ethers, Signer } from "ethers";
import { BigNumberish, BytesLike, ethers, Signer } from "ethers";
// tslint:disable-next-line:no-submodule-imports
import { arrayify } from "ethers/lib/utils";
import * as hre from "hardhat";
Expand Down Expand Up @@ -85,15 +85,30 @@ export class ContractUtils {
return hre.ethers.utils.keccak256(encodedResult);
}

public static async sign(signer: Signer, hash: string, nonce: BigNumberish): Promise<string> {
const encodedResult = ethers.utils.defaultAbiCoder.encode(
public static getRequestHash(hash: string, address: string, nonce: BigNumberish): Uint8Array {
const encodedResult = hre.ethers.utils.defaultAbiCoder.encode(
["bytes32", "address", "uint256"],
[hash, await signer.getAddress(), nonce]
[hash, address, nonce]
);
const message = arrayify(ethers.utils.keccak256(encodedResult));
return arrayify(hre.ethers.utils.keccak256(encodedResult));
}

public static async signRequestHash(signer: Signer, hash: string, nonce: BigNumberish): Promise<string> {
const message = ContractUtils.getRequestHash(hash, await signer.getAddress(), nonce);
return signer.signMessage(message);
}

public static verifyRequestHash(address: string, hash: string, nonce: BigNumberish, signature: string): boolean {
const message = ContractUtils.getRequestHash(hash, address, nonce);
let res: string;
try {
res = hre.ethers.utils.verifyMessage(message, signature);
} catch (error) {
return false;
}
return res.toLowerCase() === address.toLowerCase();
}

public static getPaymentMessage(
address: string,
purchaseId: string,
Expand Down Expand Up @@ -135,7 +150,7 @@ export class ContractUtils {
shopId: string,
account: string,
nonce: BigNumberish,
signature: string
signature: BytesLike
): boolean {
const message = ContractUtils.getPaymentMessage(account, purchaseId, amount, currency, shopId, nonce);
let res: string;
Expand Down Expand Up @@ -175,4 +190,33 @@ export class ContractUtils {
}
return res.toLowerCase() === account.toLowerCase();
}

public static getChangePayablePointMessage(phone: BytesLike, address: string, nonce: BigNumberish): Uint8Array {
const encodedResult = hre.ethers.utils.defaultAbiCoder.encode(
["bytes32", "address", "uint256"],
[phone, address, nonce]
);
return arrayify(hre.ethers.utils.keccak256(encodedResult));
}

public static async signChangePayablePoint(signer: Signer, phone: BytesLike, nonce: BigNumberish): Promise<string> {
const message = ContractUtils.getChangePayablePointMessage(phone, await signer.getAddress(), nonce);
return signer.signMessage(message);
}

public static verifyChangePayablePoint(
phone: BytesLike,
account: string,
nonce: BigNumberish,
signature: BytesLike
): boolean {
const message = ContractUtils.getChangePayablePointMessage(phone, account, nonce);
let res: string;
try {
res = ethers.utils.verifyMessage(message, signature);
} catch (error) {
return false;
}
return res.toLowerCase() === account.toLowerCase();
}
}
162 changes: 161 additions & 1 deletion packages/relay/test/Endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as path from "path";
import { URL } from "url";

import * as assert from "assert";
import { BigNumber } from "ethers";
import { BigNumber, Wallet } from "ethers";

// tslint:disable-next-line:no-implicit-dependencies
import { AddressZero } from "@ethersproject/constants";
Expand Down Expand Up @@ -207,23 +207,27 @@ describe("Test of Server", function () {

interface IUserData {
phone: string;
wallet: Wallet;
address: string;
privateKey: string;
}

const userData: IUserData[] = [
{
phone: "08201012341001",
wallet: users[0],
address: users[0].address,
privateKey: users[0].privateKey,
},
{
phone: "08201012341002",
wallet: users[1],
address: users[1].address,
privateKey: users[1].privateKey,
},
{
phone: "08201012341003",
wallet: users[2],
address: users[2].address,
privateKey: users[2].privateKey,
},
Expand Down Expand Up @@ -566,4 +570,160 @@ describe("Test of Server", function () {
});
});
});

context("Test token & point relay endpoints - using phone", () => {
before("Deploy", async () => {
await deployAllContract();
});

before("Prepare Token", async () => {
for (const elem of users) {
await tokenContract.connect(deployer).transfer(elem.address, amount.value.mul(10));
}
});

before("Create Config", async () => {
config = new Config();
config.readFromFile(path.resolve(process.cwd(), "config", "config_test.yaml"));
config.contracts.tokenAddress = tokenContract.address;
config.contracts.phoneLinkerAddress = linkCollectionContract.address;
config.contracts.ledgerAddress = ledgerContract.address;

config.relay.managerKeys = [
relay1.privateKey,
relay2.privateKey,
relay3.privateKey,
relay4.privateKey,
relay5.privateKey,
];
});

before("Create TestServer", async () => {
serverURL = new URL(`http://127.0.0.1:${config.server.port}`);
server = new TestServer(config);
});

before("Start TestServer", async () => {
await server.start();
});

after("Stop TestServer", async () => {
await server.stop();
});

context("Prepare shop data", () => {
it("Add Shop Data", async () => {
for (const elem of shopData) {
const phoneHash = ContractUtils.getPhoneHash(elem.phone);
await expect(
shopCollection
.connect(validator1)
.add(elem.shopId, elem.provideWaitTime, elem.providePercent, phoneHash)
)
.to.emit(shopCollection, "AddedShop")
.withArgs(elem.shopId, elem.provideWaitTime, elem.providePercent, phoneHash);
}
expect(await shopCollection.shopsLength()).to.equal(shopData.length);
});
});

context("Save Purchase Data", () => {
const userIndex = 0;
const purchase: IPurchaseData = {
purchaseId: "P000001",
timestamp: 1672844400,
amount: 10000,
method: 0,
currency: "krw",
shopIndex: 1,
userIndex,
};

it("Save Purchase Data", async () => {
const phoneHash = ContractUtils.getPhoneHash(userData[userIndex].phone);
const userAccount = AddressZero;
const purchaseAmount = Amount.make(purchase.amount, 18).value;
const shop = shopData[purchase.shopIndex];
const pointAmount = purchaseAmount.mul(shop.providePercent).div(100);
await expect(
ledgerContract.connect(validators[0]).savePurchase({
purchaseId: purchase.purchaseId,
timestamp: purchase.timestamp,
amount: purchaseAmount,
currency: purchase.currency.toLowerCase(),
shopId: shop.shopId,
method: purchase.method,
account: userAccount,
phone: phoneHash,
})
)
.to.emit(ledgerContract, "SavedPurchase")
.withNamedArgs({
purchaseId: purchase.purchaseId,
timestamp: purchase.timestamp,
amount: purchaseAmount,
currency: purchase.currency.toLowerCase(),
shopId: shop.shopId,
method: purchase.method,
account: userAccount,
phone: phoneHash,
})
.emit(ledgerContract, "ProvidedUnPayablePoint")
.withNamedArgs({
phone: phoneHash,
providedAmountPoint: pointAmount,
value: pointAmount,
balancePoint: pointAmount,
purchaseId: purchase.purchaseId,
shopId: shop.shopId,
});
});

it("Link phone and wallet address", async () => {
const phoneHash = ContractUtils.getPhoneHash(userData[userIndex].phone);
const nonce = await linkCollectionContract.nonceOf(userData[userIndex].address);
const signature = await ContractUtils.signRequestHash(userData[userIndex].wallet, phoneHash, nonce);
const requestId = ContractUtils.getRequestId(phoneHash, userData[userIndex].address, nonce);
await expect(
linkCollectionContract
.connect(relay1)
.addRequest(requestId, phoneHash, userData[userIndex].address, signature)
)
.to.emit(linkCollectionContract, "AddedRequestItem")
.withArgs(requestId, phoneHash, userData[userIndex].address);
await linkCollectionContract.connect(linkValidators[0]).voteRequest(requestId);
await linkCollectionContract.connect(linkValidators[0]).countVote(requestId);
});

it("Change to payable point", async () => {
const phoneHash = ContractUtils.getPhoneHash(userData[userIndex].phone);
const payableBalance = await ledgerContract.pointBalanceOf(userData[userIndex].address);
const unPayableBalance = await ledgerContract.unPayablePointBalanceOf(phoneHash);

const nonce = await ledgerContract.nonceOf(userData[userIndex].address);
const signature = await ContractUtils.signChangePayablePoint(
userData[userIndex].wallet,
phoneHash,
nonce
);

const uri = URI(serverURL).directory("changeToPayablePoint");
const url = uri.toString();
const response = await client.post(url, {
phone: phoneHash,
account: users[userIndex].address,
signature,
});

expect(response.data.code).to.equal(200);
expect(response.data.data).to.not.equal(undefined);
expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i);

expect(await ledgerContract.pointBalanceOf(userData[userIndex].address)).to.equal(
payableBalance.add(unPayableBalance)
);
expect(await ledgerContract.unPayablePointBalanceOf(phoneHash)).to.equal(0);
});
});
});
});

0 comments on commit bcee3b9

Please sign in to comment.