├── LICENSE.md ├── README.md ├── SECURITY.md ├── pi_python.py └── pi_python_test.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | PiOS License 2 | 3 | Copyright (C) 2023 César Cordero Rodríguez 4 | 5 | Permission is hereby granted by the application software developer (“Software Developer”), free 6 | of charge, to any person obtaining a copy of this application, software and associated 7 | documentation files (the “Software”), which was developed by the Software Developer for use on 8 | Pi Network, whereby the purpose of this license is to permit the development of derivative works 9 | based on the Software, including the right to use, copy, modify, merge, publish, distribute, 10 | sub-license, and/or sell copies of such derivative works and any Software components incorporated 11 | therein, and to permit persons to whom such derivative works are furnished to do so, in each case, 12 | solely to develop, use and market applications for the official Pi Network. For purposes of this 13 | license, Pi Network shall mean any application, software, or other present or future platform 14 | developed, owned or managed by Pi Community Company, and its parents, affiliates or subsidiaries, 15 | for which the Software was developed, or on which the Software continues to operate. However, 16 | you are prohibited from using any portion of the Software or any derivative works thereof in any 17 | manner (a) which infringes on any Pi Network intellectual property rights, (b) to hack any of Pi 18 | Network’s systems or processes or (c) to develop any product or service which is competitive with 19 | the Pi Network. 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or 22 | substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 25 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 26 | AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS, PUBLISHERS, OR COPYRIGHT HOLDERS OF THIS 27 | SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO BUSINESS INTERRUPTION, LOSS OF USE, DATA OR PROFITS) 29 | HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 30 | TORT (INCLUDING NEGLIGENCE) ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 31 | OR OTHER DEALINGS IN THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 32 | 33 | Pi, Pi Network and the Pi logo are trademarks of the Pi Community Company. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pi Network - Python server-side package 2 | 3 | This is a user generated Python-based solution for Pi Network you can use to integrate the Pi Network apps platform with a Python backend application. 4 | 5 | pi_python.py is the Library and pi_python_test.py is to test the Library. 6 | 7 | 8 | ## Example 9 | 10 | 1. Initialize the SDK and enter your secret data 11 | ```python 12 | """ Secret Data """ 13 | api_key = "Enter Here Your API Key" 14 | wallet_private_seed = "SecretWalletSeed" 15 | 16 | """ Initialization """ 17 | pi = PiNetwork() 18 | pi.initialize(api_key, wallet_private_seed, "Pi Testnet") 19 | ``` 20 | 21 | 2. Create an A2U payment 22 | 23 | Make sure to store your payment data in your database. Here's an example of how you could keep track of the data. 24 | Consider this a database table example. 25 | 26 | | uid | product_id | amount | memo | payment_id | txid | 27 | | :---: | :---: | :---: | :---: | :---: | :---: | 28 | | `user_uid` | apple-pie-1 | 3.14 | Refund for apple pie | NULL | NULL | 29 | 30 | ```python 31 | user_uid = "GET-THIS-SECRET-DATA-FROMFRONTEND" #unique for every user 32 | 33 | 34 | """ Build your payment """ 35 | payment_data = { 36 | "amount": 3.14, 37 | "memo": "Test - Greetings from MyApp", 38 | "metadata": {"product_id": "apple-pie-1"}, 39 | "uid": user_uid 40 | } 41 | 42 | """ 43 | Create an payment 44 | It is critical that you store paymentId in your database 45 | so that you don't double-pay the same user, by keeping track of the payment. 46 | 47 | """ 48 | payment_id = pi.create_payment(payment_data) 49 | ``` 50 | 51 | 3. Store the `paymentId` in your database 52 | 53 | After creating the payment, you'll get `paymentId`, which you should be storing in your database. 54 | 55 | | uid | product_id | amount | memo | payment_id | txid | 56 | | :---: | :---: | :---: | :---: | :---: | :---: | 57 | | `user_uid` | apple-pie-1 | 3.14 | Refund for apple pie | `paymentId` | NULL | 58 | 59 | 4. Submit the payment to the Pi Blockchain 60 | ```python 61 | """It is strongly recommended that you store the txid along with the paymentId you stored earlier for your reference.""" 62 | txid = pi.submit_payment(payment_id, False) 63 | ``` 64 | 65 | 5. Store the txid in your database 66 | 67 | Similarly as you did in step 3, keep the txid along with other data. 68 | 69 | | uid | product_id | amount | memo | payment_id | txid | 70 | | :---: | :---: | :---: | :---: | :---: | :---: | 71 | | `user_uid` | apple-pie-1 | 3.14 | Refund for apple pie | `paymentId` | `txid` | 72 | 73 | 6. Complete the payment 74 | ```python 75 | payment = pi.complete_payment(payment_id, txid) 76 | ``` 77 | 78 | ## Overall flow for A2U (App-to-User) payment 79 | 80 | To create an A2U payment using the Pi Python SDK, here's an overall flow you need to follow: 81 | 82 | 1. Initialize the SDK 83 | > You'll be initializing the SDK with the Pi API Key of your app and the Private Seed of your app wallet. 84 | 85 | 2. Create an A2U payment 86 | > You can create an A2U payment using `createPayment` method. This method returns a payment identifier (payment id). 87 | 88 | 3. Store the payment id in your database 89 | > It is critical that you store the payment id, returned by `createPayment` method, in your database so that you don't double-pay the same user, by keeping track of the payment. 90 | 91 | 4. Submit the payment to the Pi Blockchain 92 | > You can submit the payment to the Pi Blockchain using `submitPayment` method. This method builds a payment transaction and submits it to the Pi Blockchain for you. Once submitted, the method returns a transaction identifier (txid). 93 | 94 | 5. Store the txid in your database 95 | > It is strongly recommended that you store the txid along with the payment id you stored earlier for your reference. 96 | 97 | 6. Complete the payment 98 | > After checking the transaction with the txid you obtained, you must complete the payment, which you can do with `completePayment` method. Upon completing, the method returns the payment object. Check the `status` field to make sure everything looks correct. 99 | 100 | ## SDK Reference 101 | 102 | This section shows you a list of available methods. 103 | ### `createPayment` 104 | 105 | This method creates an A2U payment. 106 | 107 | - Required parameter: `PaymentArgs` 108 | 109 | You need to provide 4 different data and pass them as a single object to this method. 110 | ```typescript 111 | type PaymentArgs = { 112 | amount: number // the amount of Pi you're paying to your user 113 | memo: string // a short memo that describes what the payment is about 114 | metadata: object // an arbitrary object that you can attach to this payment. This is for your own use. You should use this object as a way to link this payment with your internal business logic. 115 | uid: string // a user uid of your app. You should have access to this value if a user has authenticated on your app. 116 | } 117 | ``` 118 | 119 | - Return value: `a payment identifier (paymentId: string)` 120 | 121 | ### `submitPayment` 122 | 123 | This method creates a payment transaction and submits it to the Pi Blockchain. 124 | 125 | - Required parameter: `paymentId, pending_payments` 126 | - Return value: `a transaction identifier (txid: string)` 127 | 128 | ### `completePayment` 129 | 130 | This method completes the payment in the Pi server. 131 | 132 | - Required parameter: `paymentId, txid` 133 | - Return value: `a payment object (payment: PaymentDTO)` 134 | 135 | The method return a payment object with the following fields: 136 | 137 | ```typescript 138 | payment: PaymentDTO = { 139 | // Payment data: 140 | identifier: string, // payment identifier 141 | user_uid: string, // user's app-specific ID 142 | amount: number, // payment amount 143 | memo: string, // a string provided by the developer, shown to the user 144 | metadata: object, // an object provided by the developer for their own usage 145 | from_address: string, // sender address of the blockchain transaction 146 | to_address: string, // recipient address of the blockchain transaction 147 | direction: Direction, // direction of the payment ("user_to_app" | "app_to_user") 148 | created_at: string, // payment's creation timestamp 149 | network: string, // a network of the payment ("Pi Network" | "Pi Testnet") 150 | // Status flags representing the current state of this payment 151 | status: { 152 | developer_approved: boolean, // Server-Side Approval (automatically approved for A2U payment) 153 | transaction_verified: boolean, // blockchain transaction verified 154 | developer_completed: boolean, // Server-Side Completion (handled by the create_payment! method) 155 | cancelled: boolean, // cancelled by the developer or by Pi Network 156 | user_cancelled: boolean, // cancelled by the user 157 | }, 158 | // Blockchain transaction data: 159 | transaction: null | { // This is null if no transaction has been made yet 160 | txid: string, // id of the blockchain transaction 161 | verified: boolean, // true if the transaction matches the payment, false otherwise 162 | _link: string, // a link to the operation on the Pi Blockchain API 163 | } 164 | } 165 | ``` 166 | 167 | ### `getPayment` 168 | 169 | This method returns a payment object if it exists. 170 | 171 | - Required parameter: `paymentId` 172 | - Return value: `a payment object (payment: PaymentDTO)` 173 | 174 | ### `cancelPayment` 175 | 176 | This method cancels the payment in the Pi server. 177 | 178 | - Required parameter: `paymentId` 179 | - Return value: `a payment object (payment: PaymentDTO)` 180 | 181 | ### `getIncompleteServerPayments` 182 | 183 | This method returns the latest incomplete payment which your app has created, if present. Use this method to troubleshoot the following error: "You need to complete the ongoing payment first to create a new one." 184 | 185 | - Required parameter: `none` 186 | - Return value: `an array which contains 0 or 1 payment object (payments: Array)` 187 | 188 | If a payment is returned by this method, you must follow one of the following 3 options: 189 | 190 | 1. cancel the payment, if it is not linked with a blockchain transaction and you don't want to submit the transaction anymore 191 | 192 | 2. submit the transaction and complete the payment 193 | 194 | 3. if a blockchain transaction has been made, complete the payment 195 | 196 | If you do not know what this payment maps to in your business logic, you may use its `metadata` property to retrieve which business logic item it relates to. Remember that `metadata` is a required argument when creating a payment, and should be used as a way to link this payment to an item of your business logic. 197 | 198 | ## Troubleshooting 199 | 200 | ### Error when creating a payment: "You need to complete the ongoing payment first to create a new one." 201 | 202 | See documentation for the `getIncompleteServerPayments` above. 203 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This section is to know which versions of our project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 3.7 | Python 3.7 | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | How to report a vulnerability?. 15 | 16 | Create an Issue in this repository. Describe which version of Python and pip are you using. If you don't know that info, you can tell us the most described possible your issue. 17 | 18 | If you have any question you could write an email at latinchain.info@gmail.com. 19 | 20 | If your vurnebility is accepted, you will be asked to do a pull request or we could make the changes directly. We could make some changes to your pull request to adapt to the Project. 21 | -------------------------------------------------------------------------------- /pi_python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | For more information visit https://github.com/pi-apps/pi-python 4 | """ 5 | 6 | import requests 7 | import json 8 | import stellar_sdk as s_sdk 9 | 10 | class PiNetwork: 11 | 12 | api_key = "" 13 | client = "" 14 | account = "" 15 | base_url = "" 16 | from_address = "" 17 | open_payments = {} 18 | network = "" 19 | server = "" 20 | keypair = "" 21 | fee = "" 22 | 23 | def initialize(self, api_key, wallet_private_key, network): 24 | try: 25 | if not self.validate_private_seed_format(wallet_private_key): 26 | print("No valid private seed!") 27 | self.api_key = api_key 28 | self.load_account(wallet_private_key, network) 29 | self.base_url = "https://api.minepi.com" 30 | self.open_payments = {} 31 | self.network = network 32 | self.fee = self.server.fetch_base_fee() 33 | #self.fee = fee 34 | except: 35 | return False 36 | 37 | def get_balance(self): 38 | try: 39 | balances = self.server.accounts().account_id(self.keypair.public_key).call()["balances"] 40 | balance_found = False 41 | for i in balances: 42 | if i["asset_type"] == "native": 43 | return float(i["balance"]) 44 | 45 | return 0 46 | except: 47 | return 0 48 | 49 | def get_payment(self, payment_id): 50 | url = self.base_url + "/v2/payments/" + payment_id 51 | re = requests.get(url,headers=self.get_http_headers()) 52 | self.handle_http_response(re) 53 | 54 | def create_payment(self, payment_data): 55 | try: 56 | 57 | if not self.validate_payment_data(payment_data): 58 | if __debug__: 59 | print("No valid payments found. Creating a new one...") 60 | 61 | balances = self.server.accounts().account_id(self.keypair.public_key).call()["balances"] 62 | balance_found = False 63 | for i in balances: 64 | if i["asset_type"] == "native": 65 | balance_found = True 66 | if (float(payment_data["amount"]) + (float(self.fee) / 10000000)) > float(i["balance"]): 67 | return "" 68 | break 69 | 70 | if balance_found == False: 71 | return "" 72 | 73 | obj = { 74 | 'payment': payment_data, 75 | } 76 | 77 | obj = json.dumps(obj) 78 | url = self.base_url + "/v2/payments" 79 | res = requests.post(url, data=obj, json=obj, headers=self.get_http_headers()) 80 | parsed_response = self.handle_http_response(res) 81 | 82 | identifier = "" 83 | identifier_data = {} 84 | 85 | if 'error' in parsed_response: 86 | identifier = parsed_response['payment']["identifier"] 87 | identifier_data = parsed_response['payment'] 88 | else: 89 | identifier = parsed_response["identifier"] 90 | identifier_data = parsed_response 91 | 92 | self.open_payments[identifier] = identifier_data 93 | 94 | return identifier 95 | except: 96 | return "" 97 | 98 | def submit_payment(self, payment_id, pending_payment): 99 | if payment_id not in self.open_payments: 100 | return False 101 | if pending_payment == False or payment_id in self.open_payments: 102 | payment = self.open_payments[payment_id] 103 | else: 104 | payment = pending_payment 105 | 106 | balances = self.server.accounts().account_id(self.keypair.public_key).call()["balances"] 107 | balance_found = False 108 | for i in balances: 109 | if i["asset_type"] == "native": 110 | balance_found = True 111 | if (float(payment["amount"]) + (float(self.fee)/10000000)) > float(i["balance"]): 112 | return "" 113 | break 114 | 115 | if balance_found == False: 116 | return "" 117 | 118 | if __debug__: 119 | print("Debug_Data: Payment information\n" + str(payment)) 120 | 121 | self.set_horizon_client(payment["network"]) 122 | from_address = payment["from_address"] 123 | 124 | transaction_data = { 125 | "amount": payment["amount"], 126 | "identifier": payment["identifier"], 127 | "recipient": payment["to_address"] 128 | } 129 | 130 | transaction = self.build_a2u_transaction(payment) 131 | txid = self.submit_transaction(transaction) 132 | if payment_id in self.open_payments: 133 | del self.open_payments[payment_id] 134 | 135 | return txid 136 | 137 | 138 | def complete_payment(self, identifier, txid): 139 | if not txid: 140 | obj = {} 141 | else: 142 | obj = {"txid": txid} 143 | 144 | obj = json.dumps(obj) 145 | url = self.base_url + "/v2/payments/" + identifier + "/complete" 146 | re = requests.post(url,data=obj,json=obj,headers=self.get_http_headers()) 147 | self.handle_http_response(re) 148 | 149 | def cancel_payment(self, identifier): 150 | obj = {} 151 | obj = json.dumps(obj) 152 | url = self.base_url + "/v2/payments/" + identifier + "/cancel" 153 | re = requests.post(url,data=obj,json=obj,headers=self.get_http_headers()) 154 | self.handle_http_response(re) 155 | 156 | def get_incomplete_server_payments(self): 157 | url = self.base_url + "/v2/payments/incomplete_server_payments" 158 | re = requests.get(url,headers=self.get_http_headers()) 159 | res = self.handle_http_response(re) 160 | if not res: 161 | res = {"incomplete_server_payments": []} 162 | return res["incomplete_server_payments"] 163 | 164 | def get_http_headers(self): 165 | return {'Authorization': "Key " + self.api_key, "Content-Type": "application/json"} 166 | 167 | def handle_http_response(self, re): 168 | try: 169 | 170 | result = re.json() 171 | 172 | result_dict = json.loads(str(json.dumps(result))) 173 | if __debug__: 174 | print("HTTP-Response: " + str(re)) 175 | print("HTTP-Response Data: " + str(result_dict)) 176 | return result_dict 177 | except: 178 | return False 179 | 180 | def set_horizon_client(self, network): 181 | self.client = self.server 182 | pass 183 | 184 | def load_account(self, private_seed, network): 185 | self.keypair = s_sdk.Keypair.from_secret(private_seed) 186 | if network == "Pi Network": 187 | host = "api.mainnet.minepi.com" 188 | horizon = "https://api.mainnet.minepi.com" 189 | else: 190 | host = "api.testnet.minepi.com" 191 | horizon = "https://api.testnet.minepi.com" 192 | 193 | self.server = s_sdk.Server(horizon) 194 | self.account = self.server.load_account(self.keypair.public_key) 195 | 196 | 197 | def build_a2u_transaction(self, transaction_data): 198 | if not self.validate_payment_data(transaction_data): 199 | print("No valid transaction!") 200 | 201 | amount = str(transaction_data["amount"]) 202 | 203 | # TODO: get this from horizon 204 | fee = self.fee # 100000 # 0.01π 205 | to_address = transaction_data["to_address"] 206 | memo = transaction_data["identifier"] 207 | 208 | if __debug__: 209 | print("MEMO " + str(memo)) 210 | 211 | from_address = transaction_data["from_address"] 212 | transaction = ( 213 | s_sdk.TransactionBuilder( 214 | source_account=self.account, 215 | network_passphrase=self.network, 216 | base_fee=fee, 217 | ) 218 | .add_text_memo(memo) 219 | .append_payment_op(to_address, s_sdk.Asset.native(), amount) 220 | .set_timeout(180) 221 | .build() 222 | ) 223 | 224 | return transaction 225 | 226 | def submit_transaction(self, transaction): 227 | transaction.sign(self.keypair) 228 | response = self.server.submit_transaction(transaction) 229 | txid = response["id"] 230 | return txid 231 | 232 | def validate_payment_data(self, data): 233 | if "amount" not in data: 234 | return False 235 | elif "memo" not in data: 236 | return False 237 | elif "metadata" not in data: 238 | return False 239 | elif "user_uid" not in data: 240 | return False 241 | elif "identifier" not in data: 242 | return False 243 | elif "to_address" not in data: 244 | return False 245 | return True 246 | 247 | def validate_private_seed_format(self, seed): 248 | if not seed.upper().startswith("S"): 249 | return False 250 | elif len(seed) != 56: 251 | return False 252 | return True 253 | -------------------------------------------------------------------------------- /pi_python_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | For more information visit https://github.com/pi-apps/pi-python 4 | """ 5 | 6 | from pi_python import PiNetwork 7 | 8 | """ 9 | Your SECRET Data 10 | Visit the Pi Developer Portal to get these data 11 | 12 | DO NOT expose these values to public 13 | """ 14 | api_key = "Enter Here Your API Key" 15 | wallet_private_seed = "SecretWalletSeed" 16 | 17 | """ Initialization """ 18 | pi = PiNetwork() 19 | pi.initialize(api_key, wallet_private_seed, "Pi Testnet") 20 | 21 | """ 22 | Example Data 23 | Get the user_uid from the Frontend 24 | """ 25 | user_uid = "GET-THIS-SECRET-DATA-FROMFRONTEND" #unique for every user 26 | 27 | 28 | """ Build your payment """ 29 | payment_data = { 30 | "amount": 1, 31 | "memo": "Test - Greetings from MyApp", 32 | "metadata": {"internal_data": "My favorite ice creame"}, 33 | "uid": user_uid 34 | } 35 | 36 | """ Check for incomplete payments """ 37 | incomplete_payments = pi.get_incomplete_server_payments() 38 | 39 | if __debug__: 40 | if len(incomplete_payments) > 0: 41 | print("Found incomplete payments: ") 42 | print(str(incomplete_payments)) 43 | 44 | """ Handle incomplete payments first """ 45 | if len(incomplete_payments) > 0: 46 | for i in incomplete_payments: 47 | if i["transaction"] == None: 48 | txid = pi.submit_payment(i["identifier"], i) 49 | pi.complete_payment(i["identifier"], txid) 50 | else: 51 | pi.complete_payment(i["identifier"], i["transaction"]["txid"]) 52 | 53 | """ Create an payment """ 54 | payment_id = pi.create_payment(payment_data) 55 | 56 | """ 57 | Submit the payment and receive the txid 58 | 59 | Store the txid on your side! 60 | """ 61 | if payment_id and len(payment_id) > 0: 62 | txid = pi.submit_payment(payment_id, False) 63 | 64 | """ Complete the Payment """ 65 | if txid and len(txid) > 0: 66 | payment = pi.complete_payment(payment_id, txid) 67 | --------------------------------------------------------------------------------