즐코

지갑, 트랜잭션 구현 (3) - 지갑 저장, 트랜잭션 보내기 본문

BlockChain

지갑, 트랜잭션 구현 (3) - 지갑 저장, 트랜잭션 보내기

YJLEE_KR 2022. 6. 21. 01:33

https://yjleekr.tistory.com/80

이전 포스팅에서는 지갑 생성까지만 했었다면, 이번엔 fs를 이용해서 지갑을 생성할 때마다 특정 폴더에 저장할 것이다.

그리고 저장된 지갑의 목록을 불러와서 지갑 디테일 확인 및 해당 지갑의 개인키를 이용하여 서명도 만들고, 서명과 함께 트랜잭션을 블록체인 서버쪽에 보낼 것이다. 

 

지갑과 지갑서버 

1. 지갑 저장 및 생성된 지갑 내용 화면에 뿌리기

 

파일 저장 시엔 각 지갑의 계정을 파일명으로, 파일 내용엔 개인키가 들어가게끔 코드를 작성했다.

 

Wallet 클래스를 만들 때 생성자메소드의 인자를 받지 않았었는데, 추후 지갑들 목록에서 지갑을 가져올 때 개인키를 넣고 나머지 값을 불러올 것이므로, 인자로 개인키를 받아주는 걸로 수정한다.

(실제론 요청/응답 상에 개인키가 왔다갔다하는건 위험하기때문에 이렇게 하지 않지만, 우린 확인 차 해본것) 

 

fs.writeFileSync('저장할 파일경로를 포함한 파일명', '저장할 파일 컨텐츠') : 특정 디렉토리 상에 파일을 저장해주는 메소드 사용함

// wallet/wallet.ts

import { randomBytes } from "crypto";
import { SHA256 } from "crypto-js";
import elliptic from "elliptic";
import fs from "fs";
import path from "path";

const ec = new elliptic.ec("secp256k1");

// 어떤 운영체제에서든 똑같은 data 폴더에 지갑이 저장되게끔 폴더 위치 설정
const dir = path.join(__dirname, "../data")

export class Wallet {
  public privateKey: string;
  public publicKey: string;
  public account: string;
  public balance: number;
  
  constructor(_privateKey: string = ""){
    this.privateKey = _privateKey || this.getPrivateKey();
    this.publicKey = this.getPublicKey();
    this.account = this.getAccount();
    this.balance = 0;
    
    Wallet.createWallet(this);
  }
   static createWallet(myWallet: Wallet): void {
     const filename = path.join(dir, myWallet.account);
     const fileContent = myWallet.privateKey;
     fs.writeFileSync(filename, fileContent);
    }
  }

 

index.html, index.js 수정

아래에서 지갑생성 버튼 (wallet_btn)을 클릭하면,

새로운 지갑이 생성됨과 동시에 data 폴더 상에 계정을 파일명으로, 개인키를 내용으로 가진 파일이 새로 들어가게 된다.

// views/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script defer src="/index.js"></script>
    <title>Document</title>
  </head>
  <body>
    <h1>hello wallet</h1>
    <button id="wallet_btn">지갑생성</button>
    <form id="transaction_form">
      <ul>
        <li>recipient: <input id="to" placeholder="보낼 계정" /></li>
        <li>amount: <input id="amount" placeholder="보낼 금액" /></li>
      </ul>
      <input type="submit" value="전송" />
    </form>
    <ul id="wallet_list">
      <li>Coin : hoochuCoin</li>
      <li>
        account :
        <span class="account"></span>
      </li>
      <li>
        private key :
        <span class="privateKey"></span>
      </li>
      <li>
        public key :
        <span class="publicKey"></span>
      </li>
      <li>
        balance :
        <span class="balance"></span>
      </li>
    </ul>
    <h1>지갑목록</h1>
    <button id="wallet_list_btn">지갑목록 버튼</button>
    <div class="wallet_list2">
      <ul></ul>
    </div>
  </body>
</html>

지갑 생성 버튼 클릭 시 createWallet 이벤트가 발동하면서 '/newWallet' 으로 POST 요청이 들어간다.

받은 응답 데이터로 화면 그려주기

// public/index.js

const walletBtn = document.querySelector("#wallet_btn");

walletBtn.addEventListener('click', createWallet);

const createWallet = async () => {
    const response = await axios.post('/newWallet', null);
    view(response.data);
}

const view = (wallet) => {
  const { privateKey, publicKey, account, balance } = wallet;
  privKeySpan.innerHTML = privateKey;
  pubKeySpan.innerHTML = publicKey;
  acctSpan.innerHTML = account;
  balSpan.innerHTML = balance;
};
// wallet/server.ts

app.post("/newWallet", (req, res) => {
  res.json(new Wallet());
});

아래처럼 지갑을 생성하면 data 폴더 상에 지갑계정이 저장된다!

2. 생성된 지갑 목록 가져오기

- 지갑 목록 가져오는 버튼 만들어주고, 클릭 시 /walletList로 POST 요청이 가게끔 코드 작성

- 받은 response.data는 ['파일명(계정명)', '파일명(계정명)', '파일명(계정명)'...] 일 것이고 이걸 화면상에 리스트로 쌓이게끔 map 메서드 사용

- 각 계정명을 클릭할때마다 지갑 디테일(개인키/공개키/지갑주소/잔고)이 찍히게끔 onClick 함수 설정

// public/index.js

const walletListBtn = document.querySelector("#wallet_list_btn");

walletListBtn.addEventListener("click", getWalletList);

const getWalletList = async () => {
  const walletList = document.querySelector(".wallet_list2 > ul");
  const response = await axios.post("/walletList", null);
  const list = response.data.map((acct) => {
    return `<li onClick="getView('${acct}')">${acct}</li>`;
  });
  walletList.innerHTML = list;
};
// wallet/server.ts

app.post("/walletList", (req, res) => {
  const list = Wallet.getWalletList();
  res.json(list);
});

fs.readdirSync('파일 목록을 가져올 디렉토리경로') : 디렉토리 내부 파일 목록들이 배열 안에 담겨져서 나온다

// wallet/wallet.ts - Wallet 클래스 내부

const dir = path.join(__dirname, "../data")

  static getWalletList(): string[] {
    const files: string[] = fs.readdirSync(dir);
    return files;
  }

3. 목록 내 지갑 계정 클릭 시 해당 지갑 디테일 가져오기 

각 리스트 onClick 시 getView() 함수 발동

이 때, 라우트 매개변수 사용해 get 요청, 즉 url로 account명을 전달해서 정보를 가져온다.

// public/index.js

const getView = async (account) => {
  const response = await axios.get(`/wallet/${account}`);
  view(response.data);
};

req.params 로 account를 받아와서, Wallet 클래스 내부의 파일 내용을 가져오는 함수의 인자로 넣고 개인키를 가져온다.

즉, 파일명이 지갑주소(account)이므로, 파일명을 찾아 해당 내용인 개인키를 가져오는 것이다. 

// wallet/server.ts

app.post("/wallet/:account", (req, res) => {
  const { account } = req.params;
  const privateKey = Wallet.getWalletPrivKey(account);
  res.json(new Wallet(privateKey));
});

fs.readFileSync('읽어올 파일 경로를 포함한 파일명') : 파일 내부 내용 가져와준다.

// wallet/wallet.ts - Wallet 클래스 내부

const dir = path.join(__dirname, "../data")

  static getWalletPrivKey(_account: string): string {
    const filepath = path.join(dir, _account);
    const filecont = fs.readFileSync(filepath);
    return filecont.toString();
  }
FS (FileSystem) 메소드 정리
import fs from 'fs';

fs.writeFileSync('저장할 파일경로를 포함한 파일명', '저장할 파일 컨텐츠') : 특정 디렉토리에 파일 만들어주는 메소드
fs.readdirSync('파일 목록을 가져올 디렉토리경로') : 디렉토리 내부 파일리스트를 가져와주는 메소드
fs.readFileSync('읽어올 파일 경로를 포함한 파일명') : 파일 내부 내용 가져와주는 메소드

 

트랜잭션 블록체인 서버에 넘기기

1/ 트랜잭션 내용 만들기

트랜잭션 시 블록체인 서버에 보내는 정보는 아래와 같다.

- 보내는 사람의 정보(sender) : 공개키(publicKey), 지갑주소(account)

- 받는 사람의 정보 : 지갑주소(recipient)

- 얼마나 보낼지(amount)

// public/index.js

const txForm = document.querySelector("#transaction_form");

const submitHandler = async (e) => {
  e.preventDefault();
  const publicKey = document.querySelector(".publicKey").innerText;
  const account = document.querySelector(".account").innerText;
  const { to, amount } = e.target;
  const data = {
    sender: {
      publicKey,
      account,
    },
    recipient: to.value,
    amount: parseInt(amount.value),
  };
  await axios.post("/sendTransaction", data);
};

txForm.addEventListener("submit", submitHandler);

 

2/ 블록체인 서버에게 트랜잭션 전송 

 

1/ 트랜잭션 전송 전 서명을 만들어서 트랜잭션 내용 업데이트

트랜잭션은 결국 블록체인 네트워크 상의 노드에게 보내줘야한다. 

이 때, 해당 네트워크에 접속하려면 블록체인 서버쪽 authorization이 필요하다.

( http 기본 인증 관련 포스팅 : https://yjleekr.tistory.com/80?category=1284408)

 

2/ '서명+트랜잭션내용'을 블록체인 인터페이스를 관리하는 http 서버로 전송!

우린 axios를 통해서 블록체인 네트워크 서버 쪽에 트랜잭션 전송 요청을 할건데, 

- 매번 트랜잭션 요청마다 headers를 설정해주기엔 번거롭기도 하고,

- 매번 기본url인 'http://localhost:3000'을 반복해서 쓰기 귀찮으므로,

=> axios에서 제공해주는 메소드인 create()를 사용하여 기본 baseURL 과 headers를 가지고 있는 axios 인스턴스를 만들어준다. 

 

이렇게 하면, 매번 일일이 axios.post('요청할 url', 데이터, header설정)를 해줄 필요가 없고, request.post(데이터) 이렇게 쓰면 된다!

// wallet/server.ts

const userid = process.env.USERID || "yjleeinkr";
const userpw = process.env.USERPW || "1234";
const baseURL = process.env.BASEURL || "http://localhost:3000";
const baseAuth = Buffer.from(userid + ":" + userpw).toString("base64");

// axios.create 로 axios 인스턴스 생성
const request = axios.create({
    baseURL,
    headers: {
    	Authorization: "Basic " + baseAuth,
        "Content-type": "application/json"
    }
})

app.post("/sendTransaction", async (req, res) => {
  const { sender: { account, publicKey }, recipient, amount} = req.body;
  // 트랜잭션 송신 시 서명을 만들어서 보내준다
  const signature = Wallet.createSign(req.body);
  
  const txObject = {
    sender: publicKey,
    recipient,
    amount,
    signature
  };
  
  console.log("트랜잭션오브젝트", txObject);
  // 위에서 만든 axios 인스턴스 사용 
  const response = await request.post("/sendTransaction", txObject);
  res.json({});
});

 

createSign() : 트랜잭션으로 서명 만들기 

트랜잭션 내용 중 공개키 + 수신인지갑주소 + 송금어마운 합쳐서 hash값을 만들고, 개인키로 해쉬화해서 서명을 만든다.

// wallet/wallet.ts - Wallet 클래스 내부

const dir = path.join(__dirname, "../data")

static createSign(_obj: any): elliptic.ec.Signature {
	const {
    	sender: { account, publicKey },
        recipient,
        amount
    } = _obj;
    
    // hash 만들기 (수신인계좌 + 송금amount + 공개키)
    const hash: string = SHA256([publicKey, recipient, amount].join("")).toString();
    
    // 개인키를 통해서 hash로 서명을 만듬
    const privateKey: string = Wallet.getWalletPrivKey(account);
    const keyPair: elliptic.ec.KeyPair = ec.keyFromPrivate(privateKey);
    
    return keyPair.sign(hash, "hex");
}

 

블록체인 서버 쪽에서 트랜잭션 검증하기

이제 트랜잭션이 지갑서버에서 블록체인 인터페이스 관리 서버로 넘어왔다.

받은 트랜잭션 내용은 아래와 같다.

1/  트랜잭션 검증하기

트랜잭션을 받아서 무조건 트랜잭션 풀에 던지는 게 아니라 받은 트랜잭션을 검증하기 위한 절차가 필요하다.

자세히 말하자면, 트랜잭션 내용이 바꼈는지 (sender, recipient, amount 가 수정된 적이 없는지) 확인하는 절차를 거친다.

// index.ts (블록체인 서버)

app.post("/sendTransaction", (req, res) => {
	console.log("reqbody", req.body); // 상기 스크린샷 참고(지갑서버에서 트랜잭션이 넘어옴)
    try{
    	const rcvdTx: ReceivedTx = req.body;
        Wallet.sendTransaction(rcvdTx);
    }catch(e){
    	if( e instanceof Error) console.log(e.message);
    }
    res.json([]);
})

트랜잭션 검증 메소드인 sendTransaction()은 Wallet 클래스 내부에 작성하였다.

여기서의 Wallet은 블록체인 인터페이스의 core 내의 Wallet 클래스이다.

 

프론트단의 Wallet과 블록체인 인터페이스의 Wallet의 역할은 다르다.

- 프론트단의 Wallet 클래스 : 클라이언트용 인터페이스로, 클라이언트가 트랜잭션을 만들고 블록체인 서버로 전송해주기 위한 용도 

- 블록체인 인터페이스의 Wallet 클래스 : 서명에 대한 검증을 하고 트랜잭션 내용을 만들어주는 용도

 

sendTransaction()에선 아래와 같은 역할을 해줘야한다. 

 

1/ 서명 검증 getVerify()

 

2/ 보내는 사람의 지갑정보(공개키, 계정, 잔고)를 가질 수 있어야 한다. (지갑정보 최신화)

왜냐면, 블록체인 서버가 받은 트랜잭션(위 코드 상에서의 req.body)의 sender 정보는 오직 공개키뿐이기 때문에, 이 공개키를 이용해서 보낸이의 다른 정보들도 가져올 수 있어야한다!

const myWallet = new this(_rcvdTx.sender, _rcvdTx.signature); 해당 Wallet 클래스 (this) 를 가져오면 된다. 

 

3/ 보내는 사람의 잔고 확인 

 

4/ 트랜잭션을 만드는 과정 

 

잔고 확인과 트랜잭션 만드는 과정은 다음 포스팅에서 정리할 예정이다.

// src/core/wallet/wallet.ts

export type Signature = elliptic.ec.Signature; // 서명 타입 미리 선언

// 받은 트랜잭션 인터페이스도 미리 선언
export interface ReceivedTx {
  sender: string;
  recipient: string;
  amount: number;
  signature: Signature;
} 

export class Wallet {
  public publicKey: string;
  public account: string;
  public balance: number;
  public signature: Signature;
  
  constructor(_sender: string, _signature: Signature) {
    this.publicKey = _sender;
    this.account = this.getAcct();
    this.balance = 0;
    this.signature = _signature;
  }
  
  // 보낸이의 공개키를 가지고 계정얻기 
  getAcct(): string(){
  	return Buffer.from(this.publicKey).slice(26).toString();
  }

  static sendTransaction(_rcvdTx: ReceivedTx) {
    // 1. 서명 검증
    const verify = Wallet.getVerify(_rcvdTx);
    if (verify.isError) throw new Error(verify.error);
    
    // 2. 보내는 사람의 지갑정보(공개키/계정/잔고)를 가질 수 있어야함(지갑정보 최신화)
    const myWallet = new this(_rcvdTx.sender, _rcvdTx.signature);
    // 3. todo : balance 확인
    // 4. todo : transaction 만드는 과정
  }
  
  static getVerify(_rcvdTx: ReceivedTx): Failable<undefined, string> {
    const { sender, recipient, amount, signature } = _rcvdTx;
    const hash: string = SHA256([sender, recipient, amount].join("")).toString();

    const keyPair = ec.keyFromPublic(sender, "hex");
    const isVerified = keyPair.verify(hash, signature);
    if (!isVerified) return { isError: true, error: "서명이 올바르지 않습니다." };
    return { isError: false, value: undefined };
  }
}

 

Comments