Functional one-time payments and tests#12
Functional one-time payments and tests#12turukawa wants to merge 13 commits intointerledger:mainfrom
Conversation
`http-sfv` is deprecated in favour of `http-sf`. Replaced accordingly.
`classmethod` must return the value holding class data.
Optional fields are optional and not always returned.
These take in a variety of fixes: - Row alignment and spacing - Correct dependency import path Note that this is only the most basic of fixes and much more needs to be done to fix duplicate functions and inefficient models.
It is good practice to place all environment variables - even demo ones used in 'test' - into a `.env` file that is excluded in the `.gitignore`. This commit adds: - `pydantic-settings` to import environment variables from a `.env` - `.env.example` for others to add their own settings - `config.py` to import the settings and make them available in tests.
There are numerous changes: - Removal of all unnecessary `RootModel` classes and refactoring for simple inheritence. - Removal of duplicated `WalletAddress` class from `resource.py` and `auth.py`. - Simplification of bizarre duplication of `LimitsOutgoing1`, `LimitsOutgoing2`, etc into a single class. And multiple other quality of life fixes and improves. I have no doubt this could be improved further, but this is significantly easier to navigate and read.
The parser is useful for: - Normalising wallet addresses from the form `$ilp.` to `https://ilp`. - Converting stored private keys back to PEM format. - Verifying payment response hash to validate buyer authentication.
This is the simplest function for processing a one-off payment between a purchaser and seller. Includes: - Pydantic models defining a seller account, and a pending incoming payment transaction that can be used to store stateless interactions (i.e. generate approval link -> third-party approval -> redirect for transaction completion) - One-time payment process workflow. A README covers the context and future functionality for this feature.
New dev dependencies required: `selenium` for authenticating purchase transactions via web, and `jupyterlab` for dev testing of code. the `.env` requires a test wallet login and password to test the purchase approval workflow.
|
Other than the code specific comments. I suggest you squash the 2 black/auto-formating related commits to one. |
|
I added a bunch of comments. The model refactoring is not easily reviewable after all the changes so I am OK with merging it as it is. My only concern is that we diverge a lot from the OpenAPI specs, which is fine if that improves usability and the actual spec doesn't change much to be problematic. I suggest we move forward to have a working client and then nitpick the spec. |
|
I also found some time to pick up some work and tried to test with real world data here: #14 There is some overlap on the base model changes (I only noticed your PR after my changes unfortunately). I have a cherry-picked version of my peer-to-peer transactions example with a bunch of fixes, but lets merge this one first. Here is my working version of the end-to-end example using this PR as a base. |
|
Regarding the process implementation, overall i like the idea of abstracting it, but it feels very restrictive to a buyer/seller relationship. I would rather like to use the same abstraction but with languages that is more relevant to the existing docs to avoid confusion. |
|
Thank you @johngian! Did you end up hitting submit on the review? I cannot see the code specific comments |
| Compute Digest | ||
| """ | ||
| request.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-512": hashlib.sha512(request.content).digest()})) | ||
| request.headers["Content-Digest"] = ser({"sha-512": hashlib.sha512(request.content).digest()}) |
There was a problem hiding this comment.
Since even the http_message_signatures lib doesn't use http_sf this, this can be simplified to one less dependency with:
digest = hashlib.sha512(request.content).digest()
request.headers["Content-Digest"] = f"sha-512=:{base64.b64encode(digest).decode('ascii')}:"
| ) | ||
| response = request = self.sign_request(request,("authorization",*get_default_covered_components())) | ||
| self.http_client.send(request=request) | ||
| req_headers = {**self.get_auth_header(access_token=access_token)} |
There was a problem hiding this comment.
The actual problem is a few lines above where response = request = <blah>. I guess this is an oversight of the initial bootstrapping codebase.
| extra="forbid", | ||
| ) | ||
| startCursor: Optional[constr(min_length=1)] = Field( | ||
| startCursor: Optional[str] = Field( |
There was a problem hiding this comment.
Not that this is a big problem, but this will validate even for empty string.
| description="Cursor corresponding to the first element in the result array.", | ||
| ) | ||
| endCursor: Optional[constr(min_length=1)] = Field( | ||
| endCursor: Optional[str] = Field( |
| ) | ||
| createdAt: datetime = Field(..., description="The date and time when the outgoing payment was created.") | ||
| updatedAt: Optional[datetime] = Field(None, description="The date and time when the outgoing payment was updated.") | ||
| model_config = ConfigDict() |
There was a problem hiding this comment.
There are 2 model configs defined here. Check at the start of the class definition. Ideally I would prefer extra=forbid to be more strict on validation.
| if isinstance(wallet_address, AnyUrl): | ||
| return str(wallet_address) | ||
| wallet_address = wallet_address.strip("$").strip("/") | ||
| if wallet_address.startswith("https://"): |
There was a problem hiding this comment.
Maybe you can use urlparse instead of assuming that its a valid address and using string manipulation, after removing the $ prefix.
| except Exception: | ||
| return False | ||
|
|
||
| def convert_private_key_to_PEM(self, *, private_key: str | bytes, format: str = "PRIVATE KEY") -> str: |
There was a problem hiding this comment.
Since the private key is submitted on sdk call, instead of trying to guess what the format is, maybe you can be more explicit and require the format to be defined before hand. Then there is no need to parse the key, if its base64 or pem according to the format, if not internal cryptography is going to fail.
| return wallet_address | ||
| return f"https://{wallet_address}" | ||
|
|
||
| def isBase64(self, *, term: str | bytes) -> bool: |
There was a problem hiding this comment.
Same here, i suggest we are explicit on the format instead of trying to identify it.
| return value | ||
|
|
||
|
|
||
| class SellerOpenPaymentAccount(BaseModel): |
There was a problem hiding this comment.
I don't think that seller is a good naming pattern, since we are not only dealing with sales.
| http_client = HttpClient(http_timeout=10.0) | ||
| self.http_client = http_client | ||
| self.seller = seller | ||
| self.buyer = paymentsparser.normalise_wallet_address(wallet_address=buyer) |
There was a problem hiding this comment.
Same here, its not always a seller and a buyer, lets make it a bit more relevant to the overall API language.
This is a fairly substantial pull request to get to a working version of the SDK. The updates can be divided into the following components:
RootModelsmuch of which could be deleted and simplified.processfolder details the main objectives and roadmap, and this update implements the simplest possible payments workflow for purchase and sale.I have commented the code I've introduced, with references, and hope this is straightforward to read.
All tests green. To run your own tests, please review the
.env.exampleand add your own values.