bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 41쪽 중 32쪽
elf
.
Hash
request_id
=
1
;
aelf
.
Address
transmitter
=
2
;
}
message
RequestTypeIndexSet
{
option
(
aelf
.
is_event
)
=
true
;
int32
request_type_index
=
1
;
}
message
CommitmentDeleted
{
option
(
aelf
.
is_event
)
=
true
;
aelf
.
Hash
request_id
=
1
;
}
Oracle Common Message Contract Reference State
Create a new file
oracle_common_message.proto
under
src/Protobuf/reference/
.
Replace this code of implementation file of
oracle_common_message.proto
:
oracle_common_message.proto
// the version of the language, use proto3 for contracts
syntax
=
"proto3"
;
// some core imports for AElf chain types
import
"aelf/core.proto"
;
import
"aelf/options.proto"
;
import
"Protobuf/reference/acs12.proto"
;
package
oracle
;
// import for using the google.protobuf.* type.
import
"google/protobuf/empty.proto"
;
import
"google/protobuf/wrappers.proto"
;
import
"google/protobuf/timestamp.proto"
;
// the name of the C# namespace in which the contract code will be,
message
Commitment
{
aelf
.
Hash
request_id
=
1
;
aelf
.
Address
coordinator
=
2
;
// coordinator contract address
aelf
.
Address
client
=
3
;
// consumer contract address
int64
subscription_id
=
4
;
google
.
protobuf
.
Timestamp
timeout_timestamp
=
5
;
bytes
specific_data
=
6
;
int32
request_type_index
=
7
;
aelf
.
Hash
trace_id
=
8
;
}
message
Report
{
bytes
result
=
1
;
bytes
error
=
2
;
bytes
on_chain_metadata
=
3
;
// serialized Commitment
bytes
off_chain_metadata
=
4
;
// TODO use in getDonFee() for dynamic billing
}
VRF Coordinator Contract Reference State
Create a new file
vrf_coordinator_contract.proto
under
src/Protobuf/reference/
.
Replace this code of implementation file of
vrf_coordinator_contract.proto
:
vrf_coordinator_contract.proto
// the version of the language, use proto3 for contracts
syntax
=
"proto3"
;
package
vrf
;
// some core imports for AElf chain types
import
"aelf/core.proto"
;
import
"aelf/options.proto"
;
import
"Protobuf/reference/acs12.proto"
;
import
"Protobuf/reference/oracle_common_message.proto"
;
import
"Protobuf/reference/coordinator_contract.proto"
;
// import for using the google.protobuf.* type.
import
"google/protobuf/empty.proto"
;
import
"google/protobuf/wrappers.proto"
;
import
"google/protobuf/timestamp.proto"
;
// the name of the C# namespace in which the contract code will be,
// generated code will also be in this namespace.
option
csharp_namespace
=
"AetherLink.Contracts.VRF.Coordinator"
;
// the contract definition: a gRPC service definition.
service
VrfCoordinatorContract
{
// the full name of the C# class that will contain the state (here <namespace>.<state-class-name> format).
option
(
aelf
.
base
)
=
"Protobuf/reference/acs12.proto"
;
option
(
aelf
.
base
)
=
"Protobuf/reference/coordinator_contract.proto"
;
option
(
aelf
.
csharp_state
)
=
"AetherLink.Contracts.VRF.Coordinator.VrfCoordinatorContractState"
;
rpc
SetConfig
(
Config
)
returns
(
google
.
protobuf
.
Empty
)
{
}
rpc
GetConfig
(
google
.
protobuf
.
Empty
)
returns
(
Config
)
{
option
(
aelf
.
is_view
)
=
true
;
}
}
message
Config
{
int64
request_timeout_seconds
=
1
;
int64
minimum_request_confirmations
=
2
;
int64
max_request_confirmations
=
3
;
int64
max_num_words
=
4
;
}
message
SpecificData
{
int64
block_number
=
1
;
int64
num_words
=
2
;
// amount of random values
aelf
.
Hash
key_hash
=
3
;
// hash of the public key
int64
request_confirmations
=
4
;
// amount of blocks to wait
aelf
.
Hash
pre_seed
=
5
;
}
// log event
message
ConfigSet
{
option
(
aelf
.
is_event
)
=
true
;
Config
config
=
1
;
}
Message Request Interface Contract State
Create a new file
request_interface.proto
under
src/Protobuf/message/
.
Replace this code of implementation file of
request_interface.proto
:
request_interface.proto
// the version of the language, use proto3 for contracts
syntax
=
"proto3"
;
// some core imports for AElf chain types
import
"aelf/core.proto"
;
import
"aelf/options.proto"
;
import
"Protobuf/reference/acs12.proto"
;
package
oracle
;
// import for using the google.protobuf.* type.
import
"google/protobuf/empty.proto"
;
import
"google/protobuf/wrappers.proto"
;
import
"google/protobuf/timestamp.proto"
;
// the name of the C# namespace in which the contract code will be,
// generated code will also be in this namespace.
option
csharp_namespace
=
"AetherLink.Contracts.Consumer"
;
// the contract definition: a gRPC service definition.
service
RequestInterface
{
// the full name of the C# class that will contain the state (here <namespace>.<state-class-name> format).
rpc
StartOracleRequest
(
StartOracleRequestInput
)
returns
(
google
.
protobuf
.
Empty
)
;
rpc
HandleOracleFulfillment
(
HandleOracleFulfillmentInput
)
returns
(
google
.
protobuf
.
Empty
)
;
}
message
StartOracleRequestInput
{
int64
subscription_id
=
1
;
int32
request_type_index
=
2
;
bytes
specific_data
=
3
;
aelf
.
Hash
trace_id
=
4
;
}
message
HandleOracleFulfillmentInput
{
aelf
.
Hash
request_id
=
1
;
bytes
response
=
2
;
bytes
err
=
3
;
int32
request_type_index
=
4
;
aelf
.
Hash
trace_id
=
5
;
}
message
OracleResponse
{
bytes
response
=
1
;
bytes
err
=
2
;
}
Contract Reference State
Navigate to
src
.
create a
new file
ContractReferences.cs
.
The implementation of file
src/ContractReferences.cs
is as follows:
ContractReferences.cs
using
AElf
.
Contracts
.
MultiToken
;
using
AetherLink
.
Contracts
.
Oracle
;
namespace
AElf
.
Contracts
.
DiceMaster
{
public
partial
class
DiceMasterState
{
internal
TokenContractContainer
.
TokenContractReferenceState
TokenContract
{
get
;
set
;
}
internal
OracleContractContainer
.
OracleContractReferenceState
OracleContract
{
get
;
set
;
}
}
}
Implement dice Game Smart Contract
Navigate to
src/DiceMaster.cs
DiceMaster.cs
using
AElf
.
Contracts
.
MultiToken
;
using
AElf
.
Sdk
.
CSharp
;
using
AElf
.
Types
;
using
AetherLink
.
Contracts
.
Consumer
;
using
AetherLink
.
Contracts
.
Oracle
;
using
AetherLink
.
Contracts
.
VRF
.
Coordinator
;
using
Google
.
Protobuf
;
using
Google
.
Protobuf
.
WellKnownTypes
;
namespace
AElf
.
Contracts
.
DiceMaster
{
// Contract class must inherit the base class generated from the proto file
public
class
DiceMaster
:
DiceMasterContainer
.
DiceMasterBase
{
private
const
string
OracleContractAddress
=
"21Fh7yog1B741yioZhNAFbs3byJ97jvBmbGAPPZKZpHHog5aEg"
;
// tDVW
//private const string OracleContractAddress = "BGhrBNTPcLccaxPv6hHJrn4CHHzeMovTsrkhFse5o2nwfvQyG"; // tDVV
private
const
string
TokenSymbol
=
"ELF"
;
private
const
long
MinimumPlayAmount
=
1_000_000
;
// 0.01 ELF
private
const
long
MaximumPlayAmount
=
1_000_000_000
;
// 10 ELF
// Initializes the contract
public
override
Empty
Initialize
(
Empty
input
)
{
// Check if the contract is already initialized
Assert
(
State
.
Initialized
.
Value
==
false
,
"Already initialized."
)
;
// Set the contract state
State
.
Initialized
.
Value
=
true
;
// Set the owner address
State
.
Owner
.
Value
=
Context
.
Sender
;
// Initialize the token contract
State
.
TokenContract
.
Value
=
Context
.
GetContractAddressByName
(
SmartContractConstants
.
TokenContractSystemName
)
;
State
.
OracleContract
.
Value
=
Address
.
FromBase58
(
OracleContractAddress
)
;
return
new
Empty
(
)
;
}
public
override
Empty
HandleOracleFulfillment
(
HandleOracleFulfillmentInput
input
)
{
var
playedRecord
=
State
.
PlayedRecords
[
input
.
TraceId
]
;
if
(
playedRecord
==
null
||
playedRecord
.
Address
==
null
)
return
new
Empty
(
)
;
var
address
=
playedRecord
.
Address
;
var
blockNumber
=
playedRecord
.
BlockNumber
;
if
(
blockNumber
!=
State
.
PlayerInfos
[
address
]
.
BlockNumber
)
{
return
new
Empty
(
)
;
}
var
randomHashList
=
HashList
.
Parser
.
ParseFrom
(
input
.
Response
)
;
var
dice1
=
randomHashList
.
Data
[
0
]
.
ToInt64
(
)
%
6
;
var
dice2
=
randomHashList
.
Data
[
1
]
.
ToInt64
(
)
%
6
;
dice1
=
(
(
dice1
<
0
)
?
dice1
*
-
1
:
dice1
)
+
1
;
dice2
=
(
(
dice2
<
0
)
?
dice2
*
-
1
:
dice2
)
+
1
;
State
.
PlayerInfos
[
address
]
.
Dice1
=
dice1
;
State
.
PlayerInfos
[
address
]
.
Dice2
=
dice2
;
State
.
PlayerInfos
[
address
]
.
Pending
=
false
;
var
playAmount
=
State
.
PlayerInfos
[
address
]
.
Amount
;
if
(
IsWinner
(
dice1
,
dice2
)
)
{
// Transfer the token from the contract to the sender
State
.
TokenContract
.
Transfer
.
Send
(
new
TransferInput
{
To
=
address
,
Symbol
=
TokenSymbol
,
Amount
=
playAmount
*
2
}
)
;
State
.
PlayerInfos
[
address
]
.
Win
=
true
;
// Emit an event to notify listeners about the outcome
Context
.
Fire
(
new
PlayOutcomeEvent
{
Amount
=
playAmount
,
Won
=
playAmount
,
From
=
address
}
)
;
}
else
{
State
.
PlayerInfos
[
address
]
.
Win
=
false
;
// Emit an event to notify listeners about the outcome
Context
.
Fire
(
new
PlayOutcomeEvent
{
Amount
=
playAmount
,
Won
=
-
playAmount
,
From
=
address
}
)
;
}
return
new
Empty
(
)
;
}
// Plays the lottery game with a specified amount of tokens.
// The method checks if the play amount is within the limit.
// If the player wins, tokens are transferred from the contract to the sender and a PlayOutcomeEvent is fired with the won amount.
// If the player loses, tokens are transferred from the sender to the contract and a PlayOutcomeEvent is fired with the lost amount.
public
override
Empty
Play
(
Int64Value
input
)
{
var
playAmount
=
input
.
Value
;
// Check if input amount is within the limit
Assert
(
playAmount
is
>=
MinimumPlayAmount
and
<=
MaximumPlayAmount
,
"Invalid play amount."
)
;
// Check if the sender has enough tokens
var
balance
=
State
.
TokenContract
.
GetBalance
.
Call
(
new
GetBalanceInput
{
Owner
=
Context
.
Sender
,
Symbol
=
TokenSymbol
}
)
.
Balance
;
Assert
(
balance
>=
playAmount
,
"Insufficient balance."
)
;
// Check if the contract has enough tokens
var
contractBalance
=
State
.
TokenContract
.
GetBalance
.
Call
(
new
GetBalanceInput
{
Owner
=
Context
.
Self
,
Symbol
=
TokenSymbol
}
)
.
Balance
;
Assert
(
contractBalance
>=
playAmount
,
"Insufficient contract balance."
)
;
if
(
State
.
PlayerInfos
[
Context
.
Sender
]
==
null
)
{
State
.
PlayerInfos
[
Context
.
Sender
]
=
new
PlayerInfo
{
Pending
=
false
,
Win
=
false
,
Dice1
=
1
,
Dice2
=
1
,
Amount
=
playAmount
,
Address
=
Context
.
Sender
,
BlockNumber
=
Context
.
CurrentHeight
}
;
}
Assert
(
State
.
PlayerInfos
[
Context
.
Sender
]
.
Pending
==
false
,
"Pending result. Please wait for the result."
)
;
// use VRF to get random number
var
keyHashs
=
State
.
OracleContract
.
GetProvingKeyHashes
.
Call
(
new
Empty
(
)
)
;
var
keyHash
=
keyHashs
.
Data
[
State
.
OracleNodeId
.
Value
]
;
var
specificData
=
new
SpecificData
{
KeyHash
=
keyHash
,
NumWords
=
2
,
RequestConfirmations
=
1
}
.
ToByteString
(
)
;
var
request
=
new
SendRequestInput
{
SubscriptionId
=
State
.
SubscriptionId
.
Value
,
RequestTypeIndex
=
2
,
SpecificData
=
specificData
,
}
;
var
traceId
=
HashHelper
.
ConcatAndCompute
(
HashHelper
.
ConcatAndCompute
(
HashHelper
.
ComputeFrom
(
Context
.
CurrentBlockTime
)
,
HashHelper
.
ComputeFrom
(
Context
.
Origin
)
)
,
HashHelper
.
ComputeFrom
(
request
)
)
;
request
.
TraceId
=
traceId
;
State
.
OracleContract
.
SendRequest
.
Send
(
request
)
;
var
blockNumber
=
Context
.
CurrentHeight
;
State
.
PlayedRecords
[
traceId
]
=
new
PlayedRecord
{
Address
=
Context
.
Sender
,
BlockNumber
=
blockNumber
,
}
;
State
.
PlayerInfos
[
Context
.
Sender
]
.
Pending
=
true
;
State
.
PlayerInfos
[
Context
.
Sender
]
.
Win
=
false
;
State
.
PlayerInfos
[
Context
.
Sender
]
.
Amount
=
playAmount
;
State
.
PlayerInfos
[
Context
.
Sender
]
.
BlockNumber
=
blockNumber
;
// Transfer the token from the sender to the contract
State
.
TokenContract
.
TransferFrom
.
Send
(
new
TransferFromInput
{
From
=
Context
.
Sender
,
To
=
Context
.
Self
,
Symbol
=
TokenSymbol
,
Amount
=
playAmount
}
)
;
return
new
Empty
(
)
;
}
// Withdraws a specified amount of tokens from the contract.
// This method can only be called by the owner of the contract.
// After the tokens are transferred, a WithdrawEvent is fired to notify any listeners about the withdrawal.
public
override
Empty
Withdraw
(
Int64Value
input
)
{
AssertIsOwner
(
)
;
// Transfer the token from the contract to the sender
State
.
TokenContract
.
Transfer
.
Send
(
new
TransferInput
{
To
=
Context
.
Sender
,
Symbol
=
TokenSymbol
,
Amount
=
input
.
Value
}
)
;
// Emit an event to notify listeners about the withdrawal
Context
.
Fire
(
new
WithdrawEvent
{
Amount
=
input
.
Value
,
From
=
Context
.
Self
,
To
=
State
.
Owner
.
Value
}
)
;
return
new
Empty
(
)
;
}
public
override
Empty
SetSubscriptionId
(
Int64Value
input
)
{
AssertIsOwner
(
)
;
State
.
SubscriptionId
.
Value
=
input
.
Value
;
return
new
Empty
(
)
;
}
public
override
Empty
SetOracleNodeId
(
Int32Value
input
)
{
AssertIsOwner
(
)
;
State
.
OracleNodeId
.
Value
=
input
.
Value
;
return
new
Empty
(
)
;
}
// Deposits a specified amount of tokens into the contract.
// This method can only be called by the owner of the contract.
// After the tokens are transferred, a DepositEvent is fired to notify any listeners about the deposit.
public
override
Empty
Deposit
(
Int64Value
input
)
{
AssertIsOwner
(
)
;
// Transfer the token from the sender to the contract
State
.
TokenContract
.
TransferFrom
.
Send
(
new
TransferFromInput
{
From
=
Context
.
Sender
,
To
=
Context
.
Self
,
Symbol
=
TokenSymbol
,
Amount
=
input
.
Value
}
)
;
// Emit an event to notify listeners about the deposit
Context
.
Fire
(
new
DepositEvent
{
Amount
=
input
.
Value
,
From
=
Context
.
Sender
,
To
=
Context
.
Self
}
)
;
return
new
Empty
(
)
;
}
// Transfers the ownership of the contract to a new owner.
// This method can only be called by the current owner of the contract.
public
override
Empty
TransferOwnership
(
Address
input
)
{
AssertIsOwner
(
)
;
// Set the new owner address
State
.
Owner
.
Value
=
input
;
return
new
Empty
(
)
;
}
// A method that read the contract's play amount limit
public
override
PlayAmountLimitMessage
GetPlayAmountLimit
(
Empty
input
)
{
// Wrap the value in the return type
return
new
PlayAmountLimitMessage
{
MinimumAmount
=
MinimumPlayAmount
,
MaximumAmount
=
MaximumPlayAmount
}
;
}
// A method that read the contract's current balance
public
override
Int64Value
GetContractBalance
(
Empty
input
)
{
// Get the balance of the contract
var
balance
=
State
.
TokenContract
.
GetBalance
.
Call
(
new
GetBalanceInput
{
Owner
=
Context
.
Self
,
Symbol
=
TokenSymbol
}
)
.
Balance
;
// Wrap the value in the return type
return
new
Int64Value
{
Value
=
balance
}
;
}
// A method that read the contract's owner
public
override
StringValue
GetOwner
(
Empty
input
)
{
return
State
.
Owner
.
Value
==
null
?
new
StringValue
(
)
:
new
StringValue
{
Value
=
State
.
Owner
.
Value
.
ToBase58
(
)
}
;
}
public
override
Int64Value
GetSubscriptionId
(
Empty
input
)
{
return
new
Int64Value
{
Value
=
State
.
SubscriptionId
.
Value
}
;
}
public
override
Int32Value
GetOracleNodeId
(
Empty
input
)
{
return
new
Int32Value
{
Value
=
State
.
OracleNodeId
.
Value
}
;
}
public
override
PlayerInfo
GetPlayerInfo
(
Address
address
)
{
Assert
(
State
.
PlayerInfos
[
address
]
!=
null
,
"No player info found."
)
;
return
State
.
PlayerInfos
[
address
]
;
}
// Determines if the player is a winner.
// The player is considered a winner if he has an odd number.
private
bool
IsWinner
(
long
dice1
,
long
dice2
)
{
var
result
=
(
dice1
+
dice2
)
%
2
;
return
result
==
1
;
}
// This method is used to ensure that only the owner of the contract can perform certain actions.
// If the context sender is not the owner, an exception is thrown with the message "Unauthorized to perform the action."
private
void
AssertIsOwner
(
)
{
Assert
(
Context
.
Sender
==
State
.
Owner
.
Value
,
"Unauthorized to perform the action."
)
;
}
}
}
Building Smart Contract
Build the new code with the following commands inside
src
folder:
Terminal
dotnet build
You should see
DiceMaster.dll.patched
in the directory
dice-game/src/bin/Debug/net.6.0
Step 3 - Deploy Smart Contract
Create A Wallet
To send transactions on the aelf blockchain, you must have a wallet.
Run this command to create aelf wallet.
Terminal
aelf-command create
You will be prompted to save your account, please do
save
your account as shown below:
Terminal
? Save account info into a file?
(
Y/n
)
Y
Make sure to choose Y to save your account information.
tip
ℹ️ Note: If you do not save your account information (by selecting n or N), do not export the wallet password. Only
proceed to the next
step if you have saved your account information.
Next, enter and confirm your password. Then export your wallet password as shown below:
Linux and macOs
Windows
Terminal
export
WALLET_PASSWORD
=
"YOUR_WALLET_PASSWORD"
Command Prompt
$env
:WALLET_PASSWORD
=
"YOUR_WALLET_PASSWORD"
Acquire Testnet Tokens (Faucet) for Development
To deploy smart contracts or execute on-chain transactions on aelf, you'll require testnet ELF tokens.
Get ELF Tokens
Go to
https://faucet-ui.aelf.dev
Enter your address and click
Get Tokens
.
Deploy Smart Contract:
The smart contract needs to be deployed on the chain before users can interact with it.
Run the following command to deploy a contract. Remember to export the path of DiceMaster.dll.patched to CONTRACT_PATH.
Linux and macOs
Windows
Terminal
export
CONTRACT_PATH
=
$(
find
~+
.
-path
"*patched*"
|
head
-n
1
)
Terminal
aelf-deploy
-a
$WALLET_ADDRESS
-p
$WALLET_PASSWORD
-c
$CONTRACT_PATH
-e
https://tdvw-test-node.aelf.io/
Command Prompt
$CONTRACT_PATH
=
Get-ChildItem
-Recurse
-Filter
"*patched*"
|
Select-Object
-First
1
-ExpandProperty
FullName
$env
:CONTRACT_PATH
=
$CONTRACT_PATH
Command Prompt
aelf-deploy
-a
$env
:WALLET_ADDRESS
-p
$env
:WALLET_PASSWORD
-c
$env
:CONTRACT_PATH
-e
https://tdvw-test-node.aelf.io/
Please wait for approximately 1 to 2 minutes. If the deployment is successful, it will provide you with the contract address.
Copy the smart contract address from the
address
field
Export your smart contract address:
Linux and macOs
Windows
Terminal
export
CONTRACT_ADDRESS
=
"YOUR_SMART_CONTRACT_ADDRESS e.g. 2LUmicHyH4RXrMjG4beDwuDsiWJESyLkgkwPdGTR8kahRzq5XS"
Command Prompt
$env
:CONTRACT_ADDRESS
=
"YOUR_SMART_CONTRACT_ADDRESS e.g. 2LUmicHyH4RXrMjG4beDwuDsiWJESyLkgkwPdGTR8kahRzq5XS"
tip
ℹ️ Note: You are to copy the smart contract address as we will be referencing it in the next steps!
info
🎉 You have successfully deployed your dApp smart contract on the aelf testnet! In the next steps, we will be building the frontend components that allow us to interact with our deployed smart contract!
Step 4 - Interact with Your Deployed Smart Contract
Approving Smart Contract Spending
Terminal
aelf-command send ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx
-a
$WALLET_ADDRESS
-p
$WALLET_PASSWORD
-e
https://tdvw-test-node.aelf.io Approve
tip
ℹ️ Note:
ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx
is the contract address of
Multitoken Contract
on aelf Testnet dAppChain (tDVW).
When prompted, enter the following parameters to approve the spending of 90 ELF tokens:프로젝트로 돌아가기