Benvenuti al secondo workshop per sviluppatori! Se vi siete persi il primo, ci siamo tuffati nelle basi del linguaggio di programmazione Ralph, insieme ai suoi strumenti di compilazione SDK e CLI, creando un semplice rubinetto a gettoni. È stata un’esperienza fantastica, puoi trovarla qui o su YouTube e GitHub.
In risposta al feedback del nostro workshop precedente, abbiamo accorciato questa sessione, dividendola in due. L’obiettivo di questo workshop è quello di re-implementare la parte di smart contract di una dApp chiamata Friend.Tech. Questa volta non partiamo da zero, ma da uno smart contract solido.
Che cos’è Friend.Tech?
Friend.tech opera come una piattaforma decentralizzata basata su token sociali lanciata nell’agosto 2023 sulla blockchain di Base. Consente ai suoi utenti di scambiare “chiavi”, precedentemente chiamate “azioni”, associate a X (ex Twitter) profili. Il possesso di queste chiavi garantisce l’accesso a chat room esclusive e contenuti speciali dal rispettivo titolare dell’account X.
La piattaforma si promuove dicendo: “Fai trading con i tuoi amici nel nostro mercato”. I fan possono acquistare e vendere azioni, con il prezzo direttamente legato al numero di azioni in circolazione. Ciò crea un’opportunità per i primi sostenitori di trarre profitto man mano che la base di fan dell’individuo cresce. Si tratta di un esperimento interessante nella narrativa del trend dei token sociali.
Perché Friend.Tech per il workshop di sviluppo?
Popolarità: Friend Tech ha guadagnato una trazione significativa nel settore delle criptovalute, generando circa 500k di entrate giornaliere del protocollo e rappresentando il 18% di tutte le transazioni sulla blockchain BASE. È un esempio interessante di un caso d’uso reale su un backend alimentato da criptovalute.
Semplicità: Nonostante la sua popolarità, lo smart contract per Friend Tech non è eccessivamente complicato, il che lo rende un ottimo esempio per scopi educativi.
Sofisticazione: Sebbene semplice, il contratto è abbastanza sofisticato da dimostrare le funzionalità avanzate di Ralph, come i subappalti e il sistema di autorizzazione delle risorse (APS). Soprattutto su come APS renda più semplice e sicuro la creazione di dApp su Alephium.
Che aspetto ha lo Smart Contract di Solidity?
Il Friend.Tech smart contract è disponibile qui. In questa sezione, spiegheremo alcune delle sue caratteristiche degne di nota, in modo che sia più facile capire l’implementazione di Ralph.
Mapping
Il contratto di solidità ha due mappature (un modo per memorizzare i valori): una rappresenta il saldo di tutti i detentori di un soggetto specifico. E altro che rappresenta il totale delle azioni in circolazione di un soggetto. Quest’ultimo viene utilizzato per calcolare i prezzi di acquisto e di vendita.
// SharesSubject => (Holder => Balance)
mapping(address => mapping(address => uint256)) public sharesBalance;
// SharesSubject => Supply
mapping(address => uint256) public sharesSupply;
Funzioni
Ci sono anche funzioni per impostare la destinazione della tariffa, la percentuale della tariffa del protocollo e la percentuale della tariffa del soggetto. Il primo imposta l’indirizzo che riceverà le tariffe del protocollo. Gli altri due stabiliscono la percentuale delle commissioni. A questi può accedere solo il proprietario dello smart contract.
function setFeeDestination(address _feeDestination) public onlyOwner {
protocolFeeDestination = _feeDestination;
}
function setProtocolFeePercent(uint256 _feePercent) public onlyOwner {
protocolFeePercent = _feePercent;
}
function setSubjectFeePercent(uint256 _feePercent) public onlyOwner {
subjectFeePercent = _feePercent;
}
La funzione successiva è “GetPrice” che calcola il prezzo delle azioni in base al numero di azioni esistenti e a quante l’acquirente vuole acquistare in quel momento.
function getPrice(uint256 supply, uint256 amount) public pure returns (uint256) {
uint256 sum1 = supply == 0 ? 0 : (supply - 1 )* (supply) * (2 * (supply - 1) + 1) / 6;
uint256 sum2 = supply == 0 && amount == 1 ? 0 : (supply - 1 + amount) * (supply + amount) * (2 * (supply - 1 + amount) + 1) / 6;
uint256 summation = sum2 - sum1;
return summation * 1 ether / 16000;
}
Il contratto ha anche il “GetBuyPrice” e il “GetSellPrice” che utilizzeranno la mappatura delle azioni in circolazione per presentare i prezzi. Questo verrà utilizzato per le prossime due funzioni che calcolano i prezzi, considerando non solo le variazioni di prezzo ma anche le commissioni che devono essere pagate dagli acquirenti e dai venditori.
function getBuyPrice(address sharesSubject, uint256 amount) public view returns (uint256) {
return getPrice(sharesSupply[sharesSubject], amount);
}
function getSellPrice(address sharesSubject, uint256 amount) public view returns (uint256) {
return getPrice(sharesSupply[sharesSubject] - amount, amount);
}
function getBuyPriceAfterFee(address sharesSubject, uint256 amount) public view returns (uint256) {
uint256 price = getBuyPrice(sharesSubject, amount);
uint256 protocolFee = price * protocolFeePercent / 1 ether;
uint256 subjectFee = price * subjectFeePercent / 1 ether;
return price + protocolFee + subjectFee;
}
function getSellPriceAfterFee(address sharesSubject, uint256 amount) public view returns (uint256) {
uint256 price = getSellPrice(sharesSubject, amount);
uint256 protocolFee = price * protocolFeePercent / 1 ether;
uint256 subjectFee = price * subjectFeePercent / 1 ether;
return price - protocolFee - subjectFee;
}
Infine, le funzioni “BuyShares” e “SellShares” sposteranno le azioni e le commissioni alla destinazione corretta quando la transazione viene eseguita.
function buyShares(address sharesSubject, uint256 amount) public payable {
uint256 supply = sharesSupply[sharesSubject];
require(supply > 0 || sharesSubject == msg.sender, "Only the shares' subject can buy the first share");
uint256 price = getPrice(supply, amount);
uint256 protocolFee = price * protocolFeePercent / 1 ether;
uint256 subjectFee = price * subjectFeePercent / 1 ether;
require(msg.value >= price + protocolFee + subjectFee, "Insufficient payment");
sharesBalance[sharesSubject][msg.sender] = sharesBalance[sharesSubject][msg.sender] + amount;
sharesSupply[sharesSubject] = supply + amount;
emit Trade(msg.sender, sharesSubject, true, amount, price, protocolFee, subjectFee, supply + amount);
(bool success1, ) = protocolFeeDestination.call{value: protocolFee}("");
(bool success2, ) = sharesSubject.call{value: subjectFee}("");
require(success1 && success2, "Unable to send funds");
}
function sellShares(address sharesSubject, uint256 amount) public payable {
uint256 supply = sharesSupply[sharesSubject];
require(supply > amount, "Cannot sell the last share");
uint256 price = getPrice(supply - amount, amount);
uint256 protocolFee = price * protocolFeePercent / 1 ether;
uint256 subjectFee = price * subjectFeePercent / 1 ether;
require(sharesBalance[sharesSubject][msg.sender] >= amount, "Insufficient shares");
sharesBalance[sharesSubject][msg.sender] = sharesBalance[sharesSubject][msg.sender] - amount;
sharesSupply[sharesSubject] = supply - amount;
emit Trade(msg.sender, sharesSubject, false, amount, price, protocolFee, subjectFee, supply - amount);
(bool success1, ) = msg.sender.call{value: price - protocolFee - subjectFee}("");
(bool success2, ) = protocolFeeDestination.call{value: protocolFee}("");
(bool success3, ) = sharesSubject.call{value: subjectFee}("");
require(success1 && success2 && success3, "Unable to send funds");
}
Dopo questa panoramica, è il momento di implementarlo su Ralph! Prima di iniziare, assicurati che la tua configurazione sia pronta: tutto il software installato per un ambiente di sviluppo pulito.
Configurazione dell’ambiente – Procedura di installazione
1 — Docker e Docker-Compose: Docker è necessario per la containerizzazione e Docker Compose è essenziale per orchestrare i contenitori. Assicurarsi di avere sia Docker che Docker Compose installati nel computer.
2 — Npm e nvm: per utilizzare gli strumenti a riga di comando per aiutarti a installare i diversi pacchetti JavaScript e gestire le loro dipendenze.
3 — Browser Extension Wallet (Chrome, Firefox): lo utilizzerai per interagire con lo smart contract.
4 — Clonare il repository GitHub del workshop per accedere a tutti i file necessari per seguire questo workshop.
Il workshop presuppone che tu abbia almeno una certa familiarità con l’interfaccia della riga di comando; Se non lo fai, puoi trovare aiuto qui.
Implementazione di Ralph
Puoi seguire il workshop completo e dettagliato nel seguente video. Di seguito è riportato il codice completo dell’implementazione di Ralph e questo articolo evidenzia i passaggi principali per una comprensione più semplice:
Passaggio 1: proprietà del contratto
Il primo passo inizia con uno smart contract minimalista Friend.Tech composto da un singolo campo leggibile e da una funzione per aggiornare il campo proprietario. Dotato di una distribuzione, è disponibile una distribuzione e uno script di transazione per chiamarlo, oltre a un semplice test per verificare la funzionalità di aggiornamento della proprietà.
Gli script di transazione sono uno strumento flessibile per chiamare gli smart contract e, se vuoi maggiori informazioni su di essi, puoi controllare il primo workshop di sviluppo, dove sono stati spiegati in modo più dettagliato.
@using(updateFields = true)
pub fn setProtocolFeePercent(feePercent: U256) -> () {
checkCaller!(callerAddress!() == owner, ErrorCodes.OwnerAllowedOnly)
protocolFeePercent = feePercent
}
Fase 2: Implementazione delle percentuali di commissione
Questo contratto ha tre campi leggibili per le percentuali delle commissioni e 2 funzioni. Queste funzioni aggiornano il protocollo e le tariffe delle materie; Solo il proprietario del contratto può chiamarli. Uno dei campi leggibili è nuovo (totalProtocolFees): la commissione di protocollo viene inviata al contratto di Friend.Tech stesso e ‘totalProtocolFees’ tiene traccia di tutte le commissioni di protocollo disponibili pronte per essere riscosse dal proprietario.
mut owner: Address,
mut totalProtocolFee: U256,
mut protocolFeePercent: U256, // basis point
mut subjectFeePercent: U256 // basis point
@using(updateFields = true)
pub fn setProtocolFeePercent(feePercent: U256) -> () {
checkCaller!(callerAddress!() == owner, ErrorCodes.OwnerAllowedOnly)
protocolFeePercent = feePercent
}
@using(updateFields = true)
pub fn setSubjectFeePercent(feePercent: U256) -> () {
checkCaller!(callerAddress!() == owner, ErrorCodes.OwnerAllowedOnly)
subjectFeePercent = feePercent
}
Poiché Alephium è basato su UTXO, una soluzione come quella sul codice Solidity creerebbe un UTXO per ogni transazione, il che è inefficiente. Una soluzione migliore è quella di creare questo campo all’interno dello smart contract e consentire solo al proprietario di ritirarlo. È stata introdotta una funzione specifica per gestire questa transazione.
@using(updateFields = true)
pub fn withdrawProtocolFee(to: Address) -> () {
checkCaller!(callerAddress!() == owner, ErrorCodes.OwnerAllowedOnly)
totalProtocolFee = 0
transferTokenFromSelf!(to, ALPH, totalProtocolFee)
}
Inoltre, è stato aggiunto un nuovo test per garantire che solo il proprietario potesse aggiornare le percentuali delle commissioni.
Passaggio 3: gestione dei saldi azionari
In questa fase, viene implementata una struttura per tenere traccia dei saldi delle azioni utilizzando la funzione di subappalto di Ralph, sostituendo la struttura dei dati di mappatura utilizzata nel codice Solidity originale. La nostra struttura consisteva in un subappalto SubjectShares e in un subappalto SubjectShareBalance per tenere traccia rispettivamente dell’offerta totale e dei saldi dei singoli detentori. In questo modo le dimensioni del contratto vengono ridotte e più flessibili rispetto alla struttura dei dati di mapping.
pub fn getSupply(sharesSubject: Address) -> U256 {
let subjectSharesContractId = subContractId!(toByteVec!(sharesSubject))
return if (contractExists!(subjectSharesContractId)) SubjectShares(subjectSharesContractId).getSupply() else 0
}
pub fn getBalance(sharesSubject: Address, holder: Address) -> U256 {
let subjectSharesContractId = subContractId!(toByteVec!(sharesSubject))
return if (contractExists!(subjectSharesContractId)) SubjectShares(subjectSharesContractId).getBalance(holder) else 0
}
}
Ora puoi verificare se un indirizzo specifico è presente nell’elenco dei titolari del subappalto “subjectShare”. Anche le funzioni “GetBalance” e “GetSupply” utilizzano i contratti di subappalto per restituire i valori.
Alcuni dei vantaggi dell’utilizzo dei subappalti rispetto alla mappatura
- Evitare il sovraccarico di stato di un singolo contratto
- Incentivi per gli utenti a riciclare i subappalti per mantenere la blockchain snella
I subappalti sono contratti, pertanto, possono
- Emettere / gestire le risorse
- Controllo granulare degli accessi sullo stato interno del contratto
- Controllo delle autorizzazioni delle risorse
- È più facile indicizzare i dati (Token / NFT) rispetto alla mappatura poiché possiamo utilizzare direttamente l’ID del contratto
- Più facile da implementare nella VM (tutto è un contratto)
Passaggio 4: implementazione dei prezzi
Le funzioni di determinazione dei prezzi create sono simili a quelle del contratto Solidity originale. Tuttavia, invece di utilizzare la struttura di mappatura, viene utilizzata la funzione “GetSupply” implementata in precedenza, in modo che “GetBuyPrice” e “GetSellPrice” possano recuperare il valore da essa.
pub fn getBuyPrice(sharesSubject: Address, amount: U256) -> U256 {
return getPrice(getSupply(sharesSubject), amount)
}
pub fn getSellPrice(sharesSubject: Address, amount: U256) -> U256 {
return getPrice(getSupply(sharesSubject) - amount, amount)
}
Prossimamente
Nella sessione successiva, approfondiremo l’implementazione delle funzioni di acquisto e vendita di azioni, che comporteranno anche l’aggiornamento dei saldi, creando e distruggendo così i subappalti secondo necessità. Su questi, è possibile vedere il sistema di autorizzazione delle risorse in azione.
Facci sapere su Twitter, Discord, Telegram o Reddit se hai domande!