3 parça olacak bu yazı serisinde Dapp(Decentralized application) denilen merkeziyetsiz bir blockchain uygulaması geliştireceğiz. Hands-on Labs tadında olacak. Uygulama geliştirmeye başlayarak öğrenme yolunu tercih edeceğiz. İlk yazı kontrat oluşturmak üzerine olacak.
Smart Contract ve Solidity
Smart Contract, blockchain ağı üzerinde çalışan içerisinde veriyi de saklı tutan kod parçalarıdır. Yani kodun ve verinin bir olduğu bir yapı. Bu kontratı yazmamızı sağlayan programa dilinin adı ise Solidity. Çok gelişmiş bir değildir ki zaten kontrat yazarken ihtiyaçlarımız da belli.
Smart contract'ı daha iyi anlamak için bir örnek verelim. Hepimiz internet alışverişlerimizde kargo ile ilgili sıkıntı yaşamışızdır diye düşünüyorum. Ürünlerin altında satıcılar tarafından kargoya verilme tarihleri belirtiliyor. Biz de buna göre sipariş veriyoruz. Fakat bu tarihte ürün kargolanmayabiliyor ya da ürün kargo şirketi tarafından çok geç teslim edilebiliyor. Alıcı açısından baktığımızda bu iki sorun satıcının veya kargo firmasının değildir. Çünkü bizim muhattabımız pazar yeridir. O zaman siparişimiz geç kargolanırsa veya teslim edilirse iptal etme hakkımız olmalı. Fakat üzülerek söylüyorum ki pazar yerlerine bu konuda güvenmek mümkün değil. Tekrar üzülerek soruyorum ki insan ile kuramadığımız bu güveni teknoloji ile nasıl tesis edebiliriz? Smart contract burada çok güzel bir çözüm sunuyor. Bu iş sürecini kontrat üzerinde kurguluyoruz ve bu kontratı şeffaf bir şekilde gösteriyoruz. Yani kontratta ne görüyorsak o sürecin işletileceğinden emin oluyoruz. Yayınlanan bir kontratın daha sonradan değiştirilemeyeceğini de unutmayalım.
O zaman gelin bu kontratı geliştirmeye başlayalım. Sürecimiz aşağıdaki adımları içerecek.
- Kontrat sahibi yani pazar yeri, siparişi kontrata yazacak.
- Alıcı sipariş için ödeme yapacak.
- Sipariş belirtilen tarihte ulaşmazsa alıcı siparişi iptal edebilecek ve ödemesini geri alabilecek.
Ortamın Hazırlanması
Smart contract için geliştirme ortamı sağlayan Truffle'ın kurulumunu yapıyoruz. Truffle'ı kullanarak smart contract'ların derleme, test etme ve dağıtma işlemlerini gerçekleştireceğiz.
npm install -g truffle
Makinemizde yerel blockchain ağı oluşturmak için Ganache'ı kuruyoruz. Ganache 7545 portundan erişilebilecek yerel bir blockchain ağı oluşturur. Bu ağın içerisinde 100ETH bakiyeye sahip 10 tane hesap bulunur. Testlerimizi bu hesaplar üzerinden gerçekleştireceğiz. Aşağıdaki bağlantıdan işletim sisteminize göre kurulum yapabilirsiniz.
https://www.trufflesuite.com/ganache
Kontrat Oluşturma
Aşağıdaki komut satırlarını çalıştıralım.
mkdir order-dapp-sample
cd order-dapp-sample
mkdir backend
cd backend
truffle init
code .
Uygulama dizininde contracts, migrations ve test olmak üzere üç adet klasör bulunuyor. contracts klasörü altına Orders.sol isminde bir kontrat oluşturuyoruz.
migrations klasörü altına 2_deploy_contracts.js isminde bir migrations ekliyoruz.
test klasörü altına order.js isminde bir test ekliyoruz.
truffle-config.js dosyasında network'ü düzenliyoruz.
Sipariş Ekleme ve Sorgulama Fonksiyonlarının Geliştirilmesi
Pazar yeri, siparişin id'sini, fiyatını ve teslim edilme tarihini kontrata yazacak. O yüzden Order adında struct oluşturuyoruz. C#'daki struct yapısı ile benzer.
Solidity'de Guid veri tipi olmadığı için id'yi string tipinde tutuyoruz. Solidity'de uint8'den uint256'ya kadar sayısal veri tutmamızı sağlayan veri tipleri var. totalPrice için uint256 veri tipini kullanıyoruz. Solidity'de DateTime veri tipi bulunmuyor. Unix timestamp formatında saklamak zorundayız. Bu yüzden deliveryDate için uint256 veri tipini kullanıyoruz.
Siparişin oluşturulma tarihi için createdDate, siparişin teslim edilme tarihi için deliveredDate ve siparişin durumu için status özelliklerini de ekliyoruz. status'u enum olarak tanımlıyoruz. C#'daki enum'a benzer ama sayı ataması yapılamıyor.
Siparişin id'sine göre sipariş bilgilerini saklayabilmek ve sipariş id'sine göre siparişi sorgulayabilmek için mapping tipinde orders isminde bir değişken oluşturuyoruz. C#'daki Dictionary gibi düşünebiliriz.
Blockchain'de her hesabın veya kontratın 160 bit'lik bir adresi var. Kontratı oluşturan hesabın adresini kaydetmeliyiz ki sipariş eklemeyi sadece bu hesap yapabilsin. Yani sipariş eklemeyi sadece pazar yeri yapabilsin. Hesap adresine msg.sender üzerinden erişiyoruz. Bu bilgiyi constructor fonksiyonundan alıyoruz ve address tipindeki owner değişkeninde saklıyoruz. Solidity'de adres bilgileri için address veri tipi kullanılır.
Sipariş bilgilerini alacağımız add isminde bir fonksiyon ekliyoruz. Bu bilgileri orders değişkeninde depoluyoruz. Bir eksiğimiz kaldı o da yetkilendirme. Yani bu fonksiyonu sadece kontrat sahibi çağırabilmeli. Bunun için onlyOwner isminde bir modifier oluşturuyoruz. Bu modifier içerisinde fonksiyonu çağıran hesap adresi ile kontratı oluşturan hesap adresini karşılaştırıyoruz. Uyuşmuyor ise "The sender isn't authorized" hatası fırlatıyoruz. Son olarak bu modifier'ı add fonksiyonuna ekliyoruz. Buraya sıralı şekilde bir çok modifier ekleyebiliriz. Bu modifier'lar sırasıyla çalıştırılır ve hata yoksa fonksiyon çalıştırılır.
Sipariş ile ilgili detayları dönebilmek için get isminde bir fonksiyon ekliyoruz. Bu fonksiyon parametre olarak sipariş id'sini alır. Siparişin varlığını kontrol ediyoruz yoksa hata fırlatıyoruz.
Akıllı kontratlar tekrar düzenlenemedikleri için kontratları yayınlamadan önce test etmemiz gerekli. O yüzden TDD devam ediyoruz. test klasörü altındaki orders.js'e add fonksiyonu için testler yazıyoruz.
Test çalışmaya başladığında Ganache 10 tane hesap tanımlar. Bu hesaplara 3. satırdaki accounts üzerinden erişebiliriz.
Ethereum ağında Ether, Gwei ve Wei olmak üzere üç birim var. Ether en büyük birim olmasına rağmen tüm işlemler en küçük birim Wei ile yapılır. O yüzden 8. satırda 2.5 Ether'i Wei'ye çeviyoruz.
Kontrat her zaman birinci hesap üzerinden oluşturulur. 16. satırda from parametresine ikinci hesabı veriyoruz ki hata oluşmasını bekliyoruz.
Ganache uygulamasını çalıştırıyoruz ve QuickStart Ethereum düğmesine tıklayarak yerel blockchain ağını aktif ediyoruz.
Aşağıdaki komut satırı ile testi çalıştırıyoruz.
truffle test ./test/orders.js
Ödeme Alma Fonksiyonunun Geliştirilmesi
Ödemeyi aldığımıza dair bilgileri tutmak için Payment isminde struct oluşturuyoruz. Siparişin id'si için id, ödemeyi yapan hesabın adresi için buyerAddress, ödeme tarihi için paidDate ve geri ödeme tarihi için refundedDate özelliklerini ekliyoruz. buyerAddress'i payable ile işaretliyoruz ki bu adrese geri ödeme yapabilelim. Mapping tipinde payments isminde bir değişken oluşturuyoruz.
Ödeme yapıldığında bir de event yayınlayalım. OrderPaid isminde event oluşturuyoruz. Event argümanları ise id, paidAddress, paidAmount ve date olsun.
Ödemeyi alabilmek için pay isminde bir fonksiyon ekliyoruz. Bu fonksiyon parametre olarak sipariş id'sini alır. Bu fonksiyonun add fonksiyonundan farkı ödeme alabiliyor olması. O yüzden payable anahtarını ekliyoruz. Siparişin varlığını kontrol ediyoruz yoksa hata fırlatıyoruz. Önceden ödeme yapılmışsa hata fırlatıyoruz. Daha sonra siparişin fiyatı ile ödeme tutarını karşılaştırıyoruz ve uyuşmuyorsa hata fırlatıyoruz. Ödeme tutarına msg.value üzerinden erişiyoruz.
Siparişin durumunu Paid olarak güncelliyoruz. Ödemeyi yapan hesabın adresini ve ödeme tarihini saklıyoruz. Bu fonksiyon ile ödeme yapıldığında tutar kontratta saklanır. Son olarak OrderPaid event'i yayınlıyoruz.
msg.value, fonksiyon çalıştırıldığında girilen tutarı verir.
msg.sender, fonksiyonu çalıştıran hesabın adresini verir.
test klasörü altındaki orders.js'e pay fonksiyonu için testler yazıyoruz.
pay metodu payable olarak işaretlendiği için ödeme alabilir. 16. ve 27. satırlarda value parametresine 200 Wei giriyoruz ki sipariş tutarı(2.5 Ether) ile 200 Wei uyuşmadığı için hata oluşmasını bekliyoruz.
Ödeme başarılı şekilde gerçekleştiğinde event yayınlamıştık. 37. satırda event'in yayınlanıp yayınlanmadığını kontrol ediyoruz.
Aşağıdaki komut satırı ile testi çalıştırıyoruz.
truffle test ./test/orders.js
Siparişi Teslim Etme Fonksiyonunun Geliştirilmesi
Sipariş teslim edildiğinde event yayınlayalım. OrderDelivered isminde event oluşturuyoruz. Event argümanları ise id ve date olsun.
Siparişi teslim edildi olarak işaretleyebilmek için deliver isminde bir fonksiyon ekliyoruz. Bu fonksiyon parametre olarak sipariş id'sini alır. Bu fonksiyonu da pazar yeri için yetkilendiriyoruz. add fonksiyonundaki gibi bu fonksiyona da onlyOwner modifier'ını ekliyoruz.
Siparişin varlığını kontrol ediyoruz yoksa hata fırlatıyoruz. Önceden teslim edilmişse hata fırlatıyoruz.
Siparişin durumunu Delivered olarak güncelliyoruz. Siparişin teslim tarihini ekliyoruz. Son olarak OrderDelivered event'i yayınlıyoruz.
test klasörü altındaki orders.js'e deliver fonksiyonu için testler yazıyoruz.
Aşağıdaki komut satırı ile testi çalıştırıyoruz.
truffle test ./test/orders.js
Geri Ödeme Yapma Fonksiyonunun Geliştirilmesi
Geri ödeme yapıldığında event yayınlayalım. OrderRefunded isminde event oluşturuyoruz. Event argümanları ise id ve date olsun.
İade işlemini yapabilmek için refund isminde bir fonksiyon ekliyoruz. Bu fonksiyon parametre olarak sipariş id'sini alır. Siparişin varlığını kontrol ediyoruz yoksa hata fırlatıyoruz. Eğer siparişin ödemesi yapılmadıysa hata fırlatıyoruz. Sipariş teslim edildiyse hata fırlatıyoruz. Geri ödeme yapıldıysa hata fırlatıyoruz. Ödemeyi yapan hesap ile fonksiyonu çağıran hesap aynı değilse hata fırlatıyoruz. Taahhüt edilen teslim tarihi henüz aşılmadıysa hata fırlatıyoruz.
Alıcıya geri ödemeyi yapıyoruz. Siparişin durumunu Refunded olarak güncelliyoruz. Geri ödeme tarihini ekliyoruz. Son olarak OrderRefunded event'i yayınlıyoruz.
Taahhüt edilen teslim tarihinin aşılıp aşılmadığının testini yapabilmek için block.timestamp 'i mock'lamamız lazım. Bunun için getTime isminde bir fonksiyon ekliyoruz ve o anki tarihi bu fonsiyondan dönüyoruz.
contracts klasörü altına MockedOrders.sol isminde bir kontrat ekliyoruz. Bu kontratı Orders kontratından türetiyoruz. mockTimestamp isminde bir fonksiyon ekliyoruz. Bu fonksiyon parametre olarak bir timestamp alır ve saklar. getTime fonksiyonu ise bu timestamp'i döner. Testimizi bu kontrat üzerinden yapacağız.
migrations klasörü altındaki 2_deploy_contracts.js migration'ını düzenliyoruz.
test klasörü altındaki orders.js'e refund fonksiyonu için testler yazıyoruz.
Solidity'de Gas, Gas Price ve Gas Cost kavramları var. Kontratta çalıştırılan her fonksiyonun belirli bir maliyeti olur. Sözleşmenin büyüklüğüne, çalışan koda göre Gas otomatik olarak hesaplanır. Örneğin pay metodu için 1000 Gas hesaplandı. Kontratta yapılan işlemlerin onaylanıp yeni bir blok oluşması için madencilere ihtiyaç var. Gas Price burada devreye giriyor. Madenciler Gas * GasPrice hesaplamasına göre iş yapar. Yani Gas Price ne kadar yüksek ise işlemler o kadar hızlı onaylanır :) Bu çarpımın sonucuna ise Gas Cost deniyor.
69. satırda Gas Price'ı alıyorum ki Ganache'da varsayılan değer 20000000000 Gwei.
82. satırda üçüncü hesabın bakiyesini alıyoruz. Ganache'da varsayılan 100 Ether.
85. ve 88. satırda pay ve refund fonksiyonları için harcanan Gas Cost'u hesaplıyoruz. Gas Price ile kullanılan Gas miktarı çarpılıyor.
92. satırda ikinci hesabın bakiyesini tekrar alıyoruz. 93. ve 94. satırlarda son bakiyeye yukarıda hesapladığımız Gas Cost'ları ekliyoruz. Ve 96. satırda bakiyeleri karşılaştırıyoruz ki geri ödeme başarılı şekilde yapılmış mı kontrol ediyoruz.
Aşağıdaki komut satırı ile testi çalıştırıyoruz.
truffle test ./test/orders.js
Kontratı geliştirmiş olduk. Bir sonraki yazıda web uygulamasında nasıl kullanılacağına bakacağız.
Örnek uygulamaya Github'dan erişebilirsiniz.
Kalın sağlıcakla.
Yorumlar
CompileError: TypeError: Invalid type for argument in function call. Invalid implicit conversion from address to address payable requested.
--> project:/contracts/Orders.sol:95:36:
|
95 | payments[id] = Payment(id, msg.sender, block.timestamp, 0);
| ^^^^^^^^^^
Adımları sırayla uyguladım neden kaynaklı olabilir? Solidity sürümü 0.8.17 olarak belirledim, o yüzden mi?
Payment struct'ında buyerAddress özelliği payable olarak işaretli ise pay metotunun da payable olarak işaretlenmesi gerekli. Bu sebeple conversion hatası oluşuyor olabilir.