feat: complete SBT verification

This commit is contained in:
snsd0805 2023-06-09 00:04:06 +08:00
commit 9436494d69
Signed by: snsd0805
GPG Key ID: 569349933C77A854
7 changed files with 6743 additions and 110 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
VITE_CHAIN_ID =
VITE_SBT_ADDR =

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ coverage
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.env

6081
package-lock.json generated

File diff suppressed because it is too large Load Diff

653
src/assets/SBT.json Normal file
View File

@ -0,0 +1,653 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "client",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "bank",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Borrow",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "client",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "bank",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Repay",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "client",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "bank",
"type": "address"
}
],
"name": "Warning",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
}
],
"name": "mint",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "bank",
"type": "address"
}
],
"name": "addReliableBank",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "bank",
"type": "address"
}
],
"name": "removeReliableBank",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "logBorrowing",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "logRepaying",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "logWarning",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "addr",
"type": "address"
},
{
"internalType": "uint256",
"name": "number",
"type": "uint256"
}
],
"name": "addCertificate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "listCertificate",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "addr",
"type": "address"
},
{
"internalType": "uint256",
"name": "number",
"type": "uint256"
}
],
"internalType": "struct SoulboundToken.Certificate[]",
"name": "",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getAccountNumber",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
}
]

View File

@ -24,7 +24,7 @@ export default {
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<!-- Content ... --> <!-- Content ... -->
<p>{{ successMsg }}</p> <span v-html="successMsg"></span>
</section> </section>
<footer class="modal-card-foot"> <footer class="modal-card-foot">
<RouterLink :to="link" class="button is-success" @click="$emit('closeModal')">{{ btnName }}</RouterLink> <RouterLink :to="link" class="button is-success" @click="$emit('closeModal')">{{ btnName }}</RouterLink>

View File

@ -1,13 +1,62 @@
<script> <script>
import PageTitle from '../components/PageTitle.vue'
// import detectEthereumProvider from '@metamask/detect-provider' import Web3 from 'web3';
import SBT from '@/assets/SBT.json'
import WarningModal from '../components/WarningModal.vue' import WarningModal from '../components/WarningModal.vue'
import SuccessModal from '../components/SuccessModal.vue' import SuccessModal from '../components/SuccessModal.vue'
// import { useClientStore } from '../stores/Client.js' import PageTitle from '../components/PageTitle.vue'
import { useClientStore } from '../stores/Client.js'
export default { export default {
components: { PageTitle, WarningModal, SuccessModal }, components: { WarningModal, SuccessModal, PageTitle },
name: 'PageFooter',
data() {
return {
SBTAddress: import.meta.env.VITE_SBT_ADDR,
number: 0,
warningModalStatus: false,
successModalStatus: false,
msg: '',
clientAddr: '',
web3: null,
token: null,
isWaiting: false
} }
},
async mounted() {
this.web3 = new Web3(window.ethereum)
this.clientAddr = (await this.web3.eth.getAccounts())[0]
this.web3.eth.defaultAccount = this.clientAddr
this.token = new this.web3.eth.Contract(SBT, this.SBTAddress)
},
methods: {
async check() {
var returnNumber = await this.token.methods.getAccountNumber(this.clientAddr).call()
if (returnNumber != 0 && returnNumber == this.number) {
this.successModalStatus = true
this.msg = '驗證成功!'
} else {
this.warningModalStatus = true
this.msg = '連接失敗,這並不是你的 SBT number'
}
},
async mint() {
this.isWaiting = true
try {
var result = await this.token.methods.mint(this.clientAddr).send({ from: this.clientAddr })
this.successModalStatus = true
var returnNumber = await this.token.methods.getAccountNumber(this.clientAddr).call()
this.msg = 'mint 成功! <a href="https://sepolia.etherscan.io/tx/' + result.transactionHash + '">etherscan</a><br>您的 SBT number 是 ' + returnNumber
} catch (error) {
this.warningModalStatus = true
this.msg = 'mint 失敗,您可能已經持有 SBT請在左方輸入 SBT number 進行驗證'
}
this.isWaiting = false
}
}
}
</script> </script>
<template> <template>
@ -30,24 +79,24 @@ export default {
<div class="field"> <div class="field">
<label class="label">SBT address</label> <label class="label">SBT address</label>
<div class="control"> <div class="control">
<input class="input is-info is-rounded" type="text" value="0x92E72Cc93e465810814F60957e08a8C8D1762Eb6" disabled> <input class="input is-info is-rounded" type="text" :value="SBTAddress" disabled>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Username</label> <label class="label">Username</label>
<div class="control has-icons-left has-icons-right"> <div class="control has-icons-left has-icons-right">
<input class="input is-info is-rounded" type="text" placeholder="Text input" value="bulma"> <input class="input is-info is-rounded" type="number" placeholder="Token number" v-model="number">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
</span> </span>
<span class="icon is-small is-right"> <span class="icon is-small is-right">
<i class="fas fa-check"></i> <!-- <i class="fas fa-check"></i> -->
</span> </span>
</div> </div>
<!-- <p class="help is-success">This username is available</p> --> <!-- <p class="help is-success">This username is available</p> -->
</div> </div>
<div class="column is-2 is-offset-5"> <div class="column is-2 is-offset-5">
<button class="button is-info is-outlined">驗證</button> <button class="button is-info is-outlined" @click="check">驗證</button>
</div> </div>
</div> </div>
</div> </div>
@ -58,18 +107,23 @@ export default {
<h1 class="title is-4">您還沒有 SBT</h1> <h1 class="title is-4">您還沒有 SBT</h1>
</div> </div>
<div class="block"> <div class="block">
確認後會向下方 SBT 智能合約地址請求 Mint 一個 Soulbound Token 到你的地址 向下方 SBT 智能合約地址請求 Mint 一個 Soulbound Token 到你的地址
</div> </div>
<div class="block"> <div class="block">
<div class="field"> <div class="field">
<label class="label">SBT address</label> <label class="label">SBT address</label>
<div class="control"> <div class="control">
<input class="input is-info is-rounded" type="text" value="0x92E72Cc93e465810814F60957e08a8C8D1762Eb6" disabled> <input class="input is-info is-rounded" type="text" :value="SBTAddress" disabled>
</div> </div>
</div> </div>
<div class="column is-2 is-offset-5"> <div class="column is-2 is-offset-5">
<button class="button is-info is-outlined">MINT</button> <template v-if="!isWaiting">
<button class="button is-info is-outlined" @click="mint">MINT</button>
</template>
<template v-else>
<button class="button is-info is-outlined is-loading"></button>
</template>
</div> </div>
</div> </div>
@ -77,20 +131,9 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="block">
<div class="columns">
<div class="column is-2 is-offset-4">
<button @click="detect" class="button is-primary is-fullwidth is-medium is-outlined">Link to MetaMask</button>
</div>
<div class="column is-2">
<RouterLink to="/" class="button is-danger is-fullwidth is-medium is-outlined">Cancel</RouterLink>
</div>
</div>
</div> -->
</div> </div>
</section> </section>
<WarningModal :active="warningModalStatus" :errorMsg="msg" @closeModal="warningModalStatus = false"></WarningModal> <WarningModal :active="warningModalStatus" :errorMsg="msg" @closeModal="warningModalStatus = false"></WarningModal>
<SuccessModal :active="successModalStatus" :successMsg="msg" @closeModal="successModalStatus = false" <SuccessModal :active="successModalStatus" :successMsg="msg" @closeModal="successModalStatus = false"
link="/signup/linksbt" btnName="繼續"></SuccessModal> link="/signup/credit" btnName="繼續"></SuccessModal>
</template> </template>

View File

@ -20,18 +20,19 @@ export default {
const provider = await detectEthereumProvider() const provider = await detectEthereumProvider()
if (provider) { if (provider) {
const chainId = await window.ethereum.request({ method: 'eth_chainId' }) const chainId = await window.ethereum.request({ method: 'eth_chainId' })
if (chainId == 0xaa36a7) { console.log(chainId)
if (chainId == import.meta.env.VITE_CHAIN_ID) {
const account = await window.ethereum.request({ method: 'eth_requestAccounts' }) const account = await window.ethereum.request({ method: 'eth_requestAccounts' })
this.client.address = account[0] this.client.address = account[0]
this.client.linked = true this.client.linked = true
this.msg = 'Success: 已經成功連接 MetaMask繼續完成 Soulbound Token 設定' this.msg = '已經成功連接 MetaMask繼續完成 Soulbound Token 設定'
this.successModalStatus = true this.successModalStatus = true
} else { } else {
this.msg = 'ERROR: 你連接的不是 Sepolia 測試網路,目前只接受 Sepolia address' this.msg = '你連接的不是 Sepolia 測試網路,目前只接受 Sepolia address'
this.warningModalStatus = true this.warningModalStatus = true
} }
} else { } else {
this.msg = 'ERROR: no Metamask' this.msg = 'no Metamask'
this.warningModalStatus = true this.warningModalStatus = true
} }
} }